Merge "Implement JS <-> TaskManager user-visible job link."
diff --git a/Android.bp b/Android.bp
index c0a2abb..3d0188a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -108,7 +108,7 @@
         ":android.security.legacykeystore-java-source",
         ":android.security.maintenance-java-source",
         ":android.security.metrics-java-source",
-        ":android.system.keystore2-V1-java-source",
+        ":android.system.keystore2-V3-java-source",
         ":credstore_aidl",
         ":dumpstate_aidl",
         ":framework_native_aidl",
diff --git a/OWNERS b/OWNERS
index d4d1936..09a721f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -15,6 +15,7 @@
 narayan@google.com #{LAST_RESORT_SUGGESTION}
 ogunwale@google.com #{LAST_RESORT_SUGGESTION}
 roosa@google.com #{LAST_RESORT_SUGGESTION}
+smoreland@google.com #{LAST_RESORT_SUGGESTION}
 svetoslavganov@android.com #{LAST_RESORT_SUGGESTION}
 svetoslavganov@google.com #{LAST_RESORT_SUGGESTION}
 yamasani@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/StubLibraries.bp b/StubLibraries.bp
index 0e08496..272b4f6 100644
--- a/StubLibraries.bp
+++ b/StubLibraries.bp
@@ -328,10 +328,12 @@
 
 java_library {
     name: "android_test_stubs_current",
-    // Modules do not have test APIs, but we want to include their SystemApis, like we include
-    // the SystemApi of framework-non-updatable-sources.
     static_libs: [
-        "all-modules-system-stubs",
+        // Updatable modules do not have test APIs, but we want to include their SystemApis, like we
+        // include the SystemApi of framework-non-updatable-sources.
+        "all-updatable-modules-system-stubs",
+        // Non-updatable modules on the other hand can have test APIs, so include their test-stubs.
+        "all-non-updatable-modules-test-stubs",
         "android-non-updatable.stubs.test",
         "private-stub-annotations-jar",
     ],
diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
index 1f66973..4242cf8 100644
--- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
+++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
@@ -109,6 +109,15 @@
     }
 
     @Override
+    public int getPendingJobReason(int jobId) {
+        try {
+            return mBinder.getPendingJobReason(jobId);
+        } catch (RemoteException e) {
+            return PENDING_JOB_REASON_UNDEFINED;
+        }
+    }
+
+    @Override
     public boolean canRunLongJobs() {
         try {
             return mBinder.canRunLongJobs(mContext.getOpPackageName());
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index a3390b7..96494ec 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -16,6 +16,7 @@
 
 package android.app.job;
 
+import android.app.Notification;
 import android.app.job.JobWorkItem;
 
 /**
@@ -104,4 +105,17 @@
      */
     void updateTransferredNetworkBytes(int jobId, in JobWorkItem item,
             long transferredDownloadBytes, long transferredUploadBytes);
+    /**
+     * Provide JobScheduler with a notification to post and tie to this job's
+     * lifecycle.
+     * This is required for all user-initiated job and optional for other jobs.
+     *
+     * @param jobId Unique integer used to identify this job.
+     * @param notificationId The ID for this notification, as per
+     *                       {@link android.app.NotificationManager#notify(int, Notification)}.
+     * @param notification The notification to be displayed.
+     * @param jobEndNotificationPolicy The policy to apply to the notification when the job stops.
+     */
+    void setNotification(int jobId, int notificationId,
+            in Notification notification, int jobEndNotificationPolicy);
 }
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
index 0b7d10b..c87a2af 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
@@ -34,6 +34,7 @@
     void cancelAll();
     ParceledListSlice getAllPendingJobs();
     JobInfo getPendingJob(int jobId);
+    int getPendingJobReason(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/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index 13b6652..659db9f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -23,10 +23,14 @@
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.usage.UsageStatsManager;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
 import android.content.ClipData;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.NetworkRequest;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.PersistableBundle;
@@ -133,6 +137,132 @@
      */
     public static final int RESULT_SUCCESS = 1;
 
+    /** The job doesn't exist. */
+    public static final int PENDING_JOB_REASON_INVALID_JOB_ID = -2;
+    /** The job is currently running and is therefore not pending. */
+    public static final int PENDING_JOB_REASON_EXECUTING = -1;
+    /**
+     * There is no known reason why the job is pending.
+     * If additional reasons are added on newer Android versions, the system may return this reason
+     * to apps whose target SDK is not high enough to expect that reason.
+     */
+    public static final int PENDING_JOB_REASON_UNDEFINED = 0;
+    /**
+     * The app is in a state that prevents the job from running
+     * (eg. the {@link JobService} component is disabled).
+     */
+    public static final int PENDING_JOB_REASON_APP = 1;
+    /**
+     * The current standby bucket prevents the job from running.
+     *
+     * @see UsageStatsManager#STANDBY_BUCKET_RESTRICTED
+     */
+    public static final int PENDING_JOB_REASON_APP_STANDBY = 2;
+    /**
+     * The app is restricted from running in the background.
+     *
+     * @see ActivityManager#isBackgroundRestricted()
+     * @see PackageManager#isInstantApp()
+     */
+    public static final int PENDING_JOB_REASON_BACKGROUND_RESTRICTION = 3;
+    /**
+     * The requested battery-not-low constraint is not satisfied.
+     *
+     * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean)
+     */
+    public static final int PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW = 4;
+    /**
+     * The requested charging constraint is not satisfied.
+     *
+     * @see JobInfo.Builder#setRequiresCharging(boolean)
+     */
+    public static final int PENDING_JOB_REASON_CONSTRAINT_CHARGING = 5;
+    /**
+     * The requested connectivity constraint is not satisfied.
+     *
+     * @see JobInfo.Builder#setRequiredNetwork(NetworkRequest)
+     * @see JobInfo.Builder#setRequiredNetworkType(int)
+     */
+    public static final int PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY = 6;
+    /**
+     * The requested content trigger constraint is not satisfied.
+     *
+     * @see JobInfo.Builder#addTriggerContentUri(JobInfo.TriggerContentUri)
+     */
+    public static final int PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER = 7;
+    /**
+     * The requested idle constraint is not satisfied.
+     *
+     * @see JobInfo.Builder#setRequiresDeviceIdle(boolean)
+     */
+    public static final int PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE = 8;
+    /**
+     * The minimum latency has not transpired.
+     *
+     * @see JobInfo.Builder#setMinimumLatency(long)
+     */
+    public static final int PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY = 9;
+    /**
+     * The system's estimate of when the app will be launched is far away enough to warrant delaying
+     * this job.
+     *
+     * @see JobInfo#isPrefetch()
+     * @see JobInfo.Builder#setPrefetch(boolean)
+     */
+    public static final int PENDING_JOB_REASON_CONSTRAINT_PREFETCH = 10;
+    /**
+     * The requested storage-not-low constraint is not satisfied.
+     *
+     * @see JobInfo.Builder#setRequiresStorageNotLow(boolean)
+     */
+    public static final int PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW = 11;
+    /**
+     * The job is being deferred due to the device state (eg. Doze, battery saver, memory usage,
+     * thermal status, etc.).
+     */
+    public static final int PENDING_JOB_REASON_DEVICE_STATE = 12;
+    /**
+     * JobScheduler thinks it can defer this job to a more optimal running time.
+     */
+    public static final int PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION = 13;
+    /**
+     * The app has consumed all of its current quota.
+     *
+     * @see UsageStatsManager#getAppStandbyBucket()
+     * @see JobParameters#STOP_REASON_QUOTA
+     */
+    public static final int PENDING_JOB_REASON_QUOTA = 14;
+    /**
+     * JobScheduler is respecting one of the user's actions (eg. force stop or adb shell commands)
+     * to defer this job.
+     */
+    public static final int PENDING_JOB_REASON_USER = 15;
+
+    /** @hide */
+    @IntDef(prefix = {"PENDING_JOB_REASON_"}, value = {
+            PENDING_JOB_REASON_UNDEFINED,
+            PENDING_JOB_REASON_APP,
+            PENDING_JOB_REASON_APP_STANDBY,
+            PENDING_JOB_REASON_BACKGROUND_RESTRICTION,
+            PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW,
+            PENDING_JOB_REASON_CONSTRAINT_CHARGING,
+            PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY,
+            PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER,
+            PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE,
+            PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY,
+            PENDING_JOB_REASON_CONSTRAINT_PREFETCH,
+            PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW,
+            PENDING_JOB_REASON_DEVICE_STATE,
+            PENDING_JOB_REASON_EXECUTING,
+            PENDING_JOB_REASON_INVALID_JOB_ID,
+            PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION,
+            PENDING_JOB_REASON_QUOTA,
+            PENDING_JOB_REASON_USER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PendingJobReason {
+    }
+
     /**
      * 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
@@ -250,6 +380,15 @@
     public abstract @Nullable JobInfo getPendingJob(int jobId);
 
     /**
+     * Returns a reason why the job is pending and not currently executing. If there are multiple
+     * reasons why a job may be pending, this will only return one of them.
+     */
+    @PendingJobReason
+    public int getPendingJobReason(int jobId) {
+        return PENDING_JOB_REASON_UNDEFINED;
+    }
+
+    /**
      * Returns {@code true} if the calling app currently holds the
      * {@link android.Manifest.permission#RUN_LONG_JOBS} permission, allowing it to run long jobs.
      */
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index bad641c..e88e979 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -19,13 +19,18 @@
 import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
 
 import android.annotation.BytesLong;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.app.Service;
 import android.compat.Compatibility;
 import android.content.Intent;
 import android.os.IBinder;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p>
  * <p>This is the base class that handles asynchronous requests that were previously scheduled. You
@@ -63,6 +68,32 @@
     public static final String PERMISSION_BIND =
             "android.permission.BIND_JOB_SERVICE";
 
+    /**
+     * Detach the notification supplied to
+     * {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
+     * The notification will remain shown even after JobScheduler stops the job.
+     *
+     * @hide
+     */
+    public static final int JOB_END_NOTIFICATION_POLICY_DETACH = 0;
+    /**
+     * Cancel and remove the notification supplied to
+     * {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
+     * The notification will be removed from the notification shade.
+     *
+     * @hide
+     */
+    public static final int JOB_END_NOTIFICATION_POLICY_REMOVE = 1;
+
+    /** @hide */
+    @IntDef(prefix = {"JOB_END_NOTIFICATION_POLICY_"}, value = {
+            JOB_END_NOTIFICATION_POLICY_DETACH,
+            JOB_END_NOTIFICATION_POLICY_REMOVE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface JobEndNotificationPolicy {
+    }
+
     private JobServiceEngine mEngine;
 
     /** @hide */
@@ -84,9 +115,9 @@
                 public long getTransferredDownloadBytes(@NonNull JobParameters params,
                         @Nullable JobWorkItem item) {
                     if (item == null) {
-                        return JobService.this.getTransferredDownloadBytes();
+                        return JobService.this.getTransferredDownloadBytes(params);
                     } else {
-                        return JobService.this.getTransferredDownloadBytes(item);
+                        return JobService.this.getTransferredDownloadBytes(params, item);
                     }
                 }
 
@@ -95,9 +126,9 @@
                 public long getTransferredUploadBytes(@NonNull JobParameters params,
                         @Nullable JobWorkItem item) {
                     if (item == null) {
-                        return JobService.this.getTransferredUploadBytes();
+                        return JobService.this.getTransferredUploadBytes(params);
                     } else {
-                        return JobService.this.getTransferredUploadBytes(item);
+                        return JobService.this.getTransferredUploadBytes(params, item);
                     }
                 }
             };
@@ -274,7 +305,7 @@
      */
     // TODO(255371817): specify the actual time JS will wait for progress before requesting
     @BytesLong
-    public long getTransferredDownloadBytes() {
+    public long getTransferredDownloadBytes(@NonNull JobParameters params) {
         if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
             // Regular jobs don't have to implement this and JobScheduler won't call this API for
             // non-data transfer jobs.
@@ -298,7 +329,7 @@
      */
     // TODO(255371817): specify the actual time JS will wait for progress before requesting
     @BytesLong
-    public long getTransferredUploadBytes() {
+    public long getTransferredUploadBytes(@NonNull JobParameters params) {
         if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
             // Regular jobs don't have to implement this and JobScheduler won't call this API for
             // non-data transfer jobs.
@@ -324,9 +355,10 @@
      */
     // TODO(255371817): specify the actual time JS will wait for progress before requesting
     @BytesLong
-    public long getTransferredDownloadBytes(@NonNull JobWorkItem item) {
+    public long getTransferredDownloadBytes(@NonNull JobParameters params,
+            @NonNull JobWorkItem item) {
         if (item == null) {
-            return getTransferredDownloadBytes();
+            return getTransferredDownloadBytes(params);
         }
         if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
             // Regular jobs don't have to implement this and JobScheduler won't call this API for
@@ -353,9 +385,10 @@
      */
     // TODO(255371817): specify the actual time JS will wait for progress before requesting
     @BytesLong
-    public long getTransferredUploadBytes(@NonNull JobWorkItem item) {
+    public long getTransferredUploadBytes(@NonNull JobParameters params,
+            @NonNull JobWorkItem item) {
         if (item == null) {
-            return getTransferredUploadBytes();
+            return getTransferredUploadBytes(params);
         }
         if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
             // Regular jobs don't have to implement this and JobScheduler won't call this API for
@@ -364,4 +397,37 @@
         }
         return 0;
     }
+
+    /**
+     * Provide JobScheduler with a notification to post and tie to this job's lifecycle.
+     * This is required for all user-initiated jobs
+     * (scheduled via {link JobInfo.Builder#setUserInitiated(boolean)}) and optional for
+     * other jobs. If the app does not call this method for a required notification within
+     * 10 seconds after {@link #onStartJob(JobParameters)} is called,
+     * the system will trigger an ANR and stop this job.
+     *
+     * <p>
+     * Note that certain types of jobs
+     * (e.g. {@link JobInfo.Builder#setDataTransfer data transfer jobs}) may require the
+     * notification to have certain characteristics and their documentation will state
+     * any such requirements.
+     *
+     * <p>
+     * JobScheduler will not remember this notification after the job has finished running,
+     * so apps must call this every time the job is started (if required or desired).
+     *
+     * @param params                   The parameters identifying this job, as supplied to
+     *                                 the job in the {@link #onStartJob(JobParameters)} callback.
+     * @param notificationId           The ID for this notification, as per
+     *                                 {@link android.app.NotificationManager#notify(int,
+     *                                 Notification)}.
+     * @param notification             The notification to be displayed.
+     * @param jobEndNotificationPolicy The policy to apply to the notification when the job stops.
+     * @hide
+     */
+    public final void setNotification(@NonNull JobParameters params, int notificationId,
+            @NonNull Notification notification,
+            @JobEndNotificationPolicy int jobEndNotificationPolicy) {
+        mEngine.setNotification(params, notificationId, notification, jobEndNotificationPolicy);
+    }
 }
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 83296a6..53e452f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -21,6 +21,7 @@
 import android.annotation.BytesLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.app.Service;
 import android.compat.Compatibility;
 import android.content.Intent;
@@ -73,6 +74,8 @@
     private static final int MSG_UPDATE_TRANSFERRED_NETWORK_BYTES = 5;
     /** Message that the client wants to update JobScheduler of the estimated transfer size. */
     private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
+    /** Message that the client wants to give JobScheduler a notification to tie to the job. */
+    private static final int MSG_SET_NOTIFICATION = 7;
 
     private final IJobService mBinder;
 
@@ -250,6 +253,24 @@
                     args.recycle();
                     break;
                 }
+                case MSG_SET_NOTIFICATION: {
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final JobParameters params = (JobParameters) args.arg1;
+                    final Notification notification = (Notification) args.arg2;
+                    IJobCallback callback = params.getCallback();
+                    if (callback != null) {
+                        try {
+                            callback.setNotification(params.getJobId(),
+                                    args.argi1, notification, args.argi2);
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "Error providing notification: binder has gone away.");
+                        }
+                    } else {
+                        Log.e(TAG, "setNotification() called for a nonexistent job.");
+                    }
+                    args.recycle();
+                    break;
+                }
                 default:
                     Log.e(TAG, "Unrecognised message received.");
                     break;
@@ -432,4 +453,27 @@
         args.argl2 = uploadBytes;
         mHandler.obtainMessage(MSG_UPDATE_ESTIMATED_NETWORK_BYTES, args).sendToTarget();
     }
-}
\ No newline at end of file
+
+    /**
+     * Give JobScheduler a notification to tie to this job's lifecycle.
+     *
+     * @hide
+     * @see JobService#setNotification(JobParameters, int, Notification, int)
+     */
+    public void setNotification(@NonNull JobParameters params, int notificationId,
+            @NonNull Notification notification,
+            @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
+        if (params == null) {
+            throw new NullPointerException("params");
+        }
+        if (notification == null) {
+            throw new NullPointerException("notification");
+        }
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = params;
+        args.arg2 = notification;
+        args.argi1 = notificationId;
+        args.argi2 = jobEndNotificationPolicy;
+        mHandler.obtainMessage(MSG_SET_NOTIFICATION, args).sendToTarget();
+    }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index dcc6aa6..e2d302f 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -4821,6 +4821,9 @@
                     Binder.restoreCallingIdentity(token);
                 }
             } else {
+                if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
+                    return -1;
+                }
                 synchronized (this) {
                     for (int j=0; j<mPowerSaveWhitelistAppsExceptIdle.size(); j++) {
                         pw.print("system-excidle,");
@@ -4882,6 +4885,9 @@
                 pw.println("[-r] requires a package name");
                 return -1;
             } else {
+                if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
+                    return -1;
+                }
                 dumpTempWhitelistSchedule(pw, false);
             }
         } else if ("except-idle-whitelist".equals(cmd)) {
@@ -4957,6 +4963,9 @@
                     Binder.restoreCallingIdentity(token);
                 }
             } else {
+                if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
+                    return -1;
+                }
                 synchronized (this) {
                     for (int j = 0; j < mPowerSaveWhitelistApps.size(); j++) {
                         pw.print(mPowerSaveWhitelistApps.keyAt(j));
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 336fb38..16201b2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -1175,7 +1175,7 @@
 
             if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime()
                     && restriction.isJobRestricted(jobStatus)) {
-                jsc.cancelExecutingJobLocked(restriction.getReason(),
+                jsc.cancelExecutingJobLocked(restriction.getStopReason(),
                         restriction.getInternalReason(),
                         JobParameters.getInternalReasonCodeDescription(
                                 restriction.getInternalReason()));
@@ -1224,7 +1224,7 @@
                 final JobRestriction restriction = mService.checkIfRestricted(running);
                 if (restriction != null) {
                     final int internalReasonCode = restriction.getInternalReason();
-                    serviceContext.cancelExecutingJobLocked(restriction.getReason(),
+                    serviceContext.cancelExecutingJobLocked(restriction.getStopReason(),
                             internalReasonCode,
                             "restricted due to "
                                     + JobParameters.getInternalReasonCodeDescription(
@@ -1340,6 +1340,7 @@
                 mActivePkgStats.add(
                         jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(),
                         packageStats);
+                mService.resetPendingJobReasonCache(jobStatus);
             }
             if (mService.getPendingJobQueue().remove(jobStatus)) {
                 mService.mJobPackageTracker.noteNonpending(jobStatus);
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 6003efe..ad6eff0 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -374,6 +374,9 @@
     @GuardedBy("mLock")
     private final ArraySet<JobStatus> mChangedJobList = new ArraySet<>();
 
+    @GuardedBy("mPendingJobReasonCache") // Use its own lock to avoid blocking JS processing
+    private final SparseArray<SparseIntArray> mPendingJobReasonCache = new SparseArray<>();
+
     /**
      * Named indices into standby bucket arrays, for clarity in referring to
      * specific buckets' bookkeeping.
@@ -1368,6 +1371,134 @@
         }
     }
 
+    @JobScheduler.PendingJobReason
+    private int getPendingJobReason(int uid, 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);
+            if (jobIdToReason != null) {
+                reason = jobIdToReason.get(jobId, JobScheduler.PENDING_JOB_REASON_UNDEFINED);
+                if (reason != JobScheduler.PENDING_JOB_REASON_UNDEFINED) {
+                    return reason;
+                }
+            }
+        }
+        synchronized (mLock) {
+            reason = getPendingJobReasonLocked(uid, jobId);
+            if (DEBUG) {
+                Slog.v(TAG, "getPendingJobReason(" + uid + "," + jobId + ")=" + reason);
+            }
+        }
+        synchronized (mPendingJobReasonCache) {
+            SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
+            if (jobIdToReason == null) {
+                jobIdToReason = new SparseIntArray();
+                mPendingJobReasonCache.put(uid, jobIdToReason);
+            }
+            jobIdToReason.put(jobId, reason);
+        }
+        return reason;
+    }
+
+    @JobScheduler.PendingJobReason
+    @GuardedBy("mLock")
+    private int getPendingJobReasonLocked(int uid, int jobId) {
+        // Very similar code to isReadyToBeExecutedLocked.
+
+        JobStatus job = mJobs.getJobByUidAndJobId(uid, jobId);
+        if (job == null) {
+            // Job doesn't exist.
+            return JobScheduler.PENDING_JOB_REASON_INVALID_JOB_ID;
+        }
+
+        if (isCurrentlyRunningLocked(job)) {
+            return JobScheduler.PENDING_JOB_REASON_EXECUTING;
+        }
+
+        final boolean jobReady = job.isReady();
+
+        if (DEBUG) {
+            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+                    + " ready=" + jobReady);
+        }
+
+        if (!jobReady) {
+            return job.getPendingJobReason();
+        }
+
+        final boolean userStarted = areUsersStartedLocked(job);
+
+        if (DEBUG) {
+            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+                    + " userStarted=" + userStarted);
+        }
+        if (!userStarted) {
+            return JobScheduler.PENDING_JOB_REASON_USER;
+        }
+
+        final boolean backingUp = mBackingUpUids.get(job.getSourceUid());
+        if (DEBUG) {
+            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+                    + " backingUp=" + backingUp);
+        }
+
+        if (backingUp) {
+            // TODO: Should we make a special reason for this?
+            return JobScheduler.PENDING_JOB_REASON_APP;
+        }
+
+        JobRestriction restriction = checkIfRestricted(job);
+        if (DEBUG) {
+            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+                    + " restriction=" + restriction);
+        }
+        if (restriction != null) {
+            return restriction.getPendingReason();
+        }
+
+        // The following can be a little more expensive (especially jobActive, since we need to
+        // go through the array of all potentially active jobs), so we are doing them
+        // later...  but still before checking with the package manager!
+        final boolean jobPending = mPendingJobQueue.contains(job);
+
+
+        if (DEBUG) {
+            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+                    + " pending=" + jobPending);
+        }
+
+        if (jobPending) {
+            // We haven't started the job for some reason. Presumably, there are too many jobs
+            // running.
+            return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+        }
+
+        final boolean jobActive = mConcurrencyManager.isJobRunningLocked(job);
+
+        if (DEBUG) {
+            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+                    + " active=" + jobActive);
+        }
+        if (jobActive) {
+            return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+        }
+
+        // Validate that the defined package+service is still present & viable.
+        final boolean componentUsable = isComponentUsable(job);
+
+        if (DEBUG) {
+            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+                    + " componentUsable=" + componentUsable);
+        }
+        if (!componentUsable) {
+            return JobScheduler.PENDING_JOB_REASON_APP;
+        }
+
+        return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+    }
+
     public JobInfo getPendingJob(int uid, int jobId) {
         synchronized (mLock) {
             ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
@@ -1408,6 +1539,9 @@
         synchronized (mLock) {
             mJobs.removeJobsOfUnlistedUsers(umi.getUserIds());
         }
+        synchronized (mPendingJobReasonCache) {
+            mPendingJobReasonCache.clear();
+        }
     }
 
     private void cancelJobsForPackageAndUidLocked(String pkgName, int uid,
@@ -1893,6 +2027,8 @@
         jobStatus.enqueueTime = sElapsedRealtimeClock.millis();
         final boolean update = lastJob != null;
         mJobs.add(jobStatus);
+        // Clear potentially cached INVALID_JOB_ID reason.
+        resetPendingJobReasonCache(jobStatus);
         if (mReadyToRock) {
             for (int i = 0; i < mControllers.size(); i++) {
                 StateController controller = mControllers.get(i);
@@ -1914,6 +2050,13 @@
         // Deal with any remaining work items in the old job.
         jobStatus.stopTrackingJobLocked(incomingJob);
 
+        synchronized (mPendingJobReasonCache) {
+            SparseIntArray reasonCache = mPendingJobReasonCache.get(jobStatus.getUid());
+            if (reasonCache != null) {
+                reasonCache.delete(jobStatus.getJobId());
+            }
+        }
+
         // Remove from store as well as controllers.
         final boolean removed = mJobs.remove(jobStatus, removeFromPersisted);
         if (!removed) {
@@ -1936,6 +2079,16 @@
         return removed;
     }
 
+    /** 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());
+            if (reasons != null) {
+                reasons.delete(jobStatus.getJobId());
+            }
+        }
+    }
+
     /** Return {@code true} if the specified job is currently executing. */
     @GuardedBy("mLock")
     public boolean isCurrentlyRunningLocked(JobStatus job) {
@@ -2230,11 +2383,20 @@
     public void onControllerStateChanged(@Nullable ArraySet<JobStatus> changedJobs) {
         if (changedJobs == null) {
             mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+            synchronized (mPendingJobReasonCache) {
+                mPendingJobReasonCache.clear();
+            }
         } else if (changedJobs.size() > 0) {
             synchronized (mLock) {
                 mChangedJobList.addAll(changedJobs);
             }
             mHandler.obtainMessage(MSG_CHECK_CHANGED_JOB_LIST).sendToTarget();
+            synchronized (mPendingJobReasonCache) {
+                for (int i = changedJobs.size() - 1; i >= 0; --i) {
+                    final JobStatus job = changedJobs.valueAt(i);
+                    resetPendingJobReasonCache(job);
+                }
+            }
         }
     }
 
@@ -2634,6 +2796,23 @@
                 if (DEBUG) {
                     Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything.");
                 }
+                final int numRunnableJobs = runnableJobs.size();
+                if (numRunnableJobs > 0) {
+                    synchronized (mPendingJobReasonCache) {
+                        for (int i = 0; i < numRunnableJobs; ++i) {
+                            final JobStatus job = runnableJobs.get(i);
+                            SparseIntArray reasons = mPendingJobReasonCache.get(job.getUid());
+                            if (reasons == null) {
+                                reasons = new SparseIntArray();
+                                mPendingJobReasonCache.put(job.getUid(), reasons);
+                            }
+                            // We're force batching these jobs, so consider it an optimization
+                            // policy reason.
+                            reasons.put(job.getJobId(),
+                                    JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION);
+                        }
+                    }
+                }
             }
 
             // Be ready for next time
@@ -3446,6 +3625,18 @@
         }
 
         @Override
+        public int getPendingJobReason(int jobId) throws RemoteException {
+            final int uid = Binder.getCallingUid();
+
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                return JobSchedulerService.this.getPendingJobReason(uid, jobId);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        @Override
         public JobInfo getPendingJob(int jobId) throws RemoteException {
             final int uid = Binder.getCallingUid();
 
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 e47d7a3..fead68e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -17,6 +17,8 @@
 package com.android.server.job;
 
 import static android.app.job.JobInfo.getPriorityString;
+import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_DETACH;
+import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_REMOVE;
 
 import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
@@ -24,6 +26,7 @@
 import android.annotation.BytesLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.app.job.IJobCallback;
 import android.app.job.IJobService;
 import android.app.job.JobInfo;
@@ -58,6 +61,7 @@
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
 import com.android.server.job.controllers.JobStatus;
+import com.android.server.notification.NotificationManagerInternal;
 import com.android.server.tare.EconomicPolicy;
 import com.android.server.tare.EconomyManagerInternal;
 import com.android.server.tare.JobSchedulerEconomicPolicy;
@@ -117,6 +121,7 @@
     private final EconomyManagerInternal mEconomyManagerInternal;
     private final JobPackageTracker mJobPackageTracker;
     private final PowerManager mPowerManager;
+    private final NotificationManagerInternal mNotificationManagerInternal;
     private PowerManager.WakeLock mWakeLock;
 
     // Execution state.
@@ -169,6 +174,11 @@
     /** The absolute maximum amount of time the job can run */
     private long mMaxExecutionTimeMillis;
 
+    private int mNotificationId;
+    private Notification mNotification;
+    private int mNotificationPid;
+    private int mNotificationJobStopPolicy;
+
     /**
      * The stop reason for a pending cancel. If there's not pending cancel, then the value should be
      * {@link JobParameters#STOP_REASON_UNDEFINED}.
@@ -235,6 +245,12 @@
                 long downloadBytes, long uploadBytes) {
             doUpdateTransferredNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
         }
+
+        @Override
+        public void setNotification(int jobId, int notificationId,
+                Notification notification, int jobEndNotificationPolicy) {
+            doSetNotification(this, jobId, notificationId, notification, jobEndNotificationPolicy);
+        }
     }
 
     JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
@@ -244,6 +260,7 @@
         mService = service;
         mBatteryStats = batteryStats;
         mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
+        mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
         mJobPackageTracker = tracker;
         mCallbackHandler = new JobServiceHandler(looper);
         mJobConcurrencyManager = concurrencyManager;
@@ -593,6 +610,49 @@
         }
     }
 
+    private void doSetNotification(JobCallback cb, int jodId, int notificationId,
+            Notification notification, int jobEndNotificationPolicy) {
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                if (!verifyCallerLocked(cb)) {
+                    return;
+                }
+                if (callingUid != mRunningJob.getUid()) {
+                    Slog.wtfStack(TAG, "Calling UID isn't the same as running job's UID...");
+                    throw new SecurityException("Can't post notification on behalf of another app");
+                }
+                if (notification == null) {
+                    throw new NullPointerException("notification");
+                }
+                if (notification.getSmallIcon() == null) {
+                    throw new IllegalArgumentException("small icon required");
+                }
+                final String callingPkgName = mRunningJob.getServiceComponent().getPackageName();
+                if (null == mNotificationManagerInternal.getNotificationChannel(
+                        callingPkgName, callingUid, notification.getChannelId())) {
+                    throw new IllegalArgumentException("invalid notification channel");
+                }
+                if (jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_DETACH
+                        && jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_REMOVE) {
+                    throw new IllegalArgumentException("invalid job end notification policy");
+                }
+                // TODO(260848384): ensure apps can't cancel the notification for user-initiated job
+                mNotificationManagerInternal.enqueueNotification(
+                        callingPkgName, callingPkgName, callingUid, callingPid, /* tag */ null,
+                        notificationId, notification, UserHandle.getUserId(callingUid));
+                mNotificationId = notificationId;
+                mNotification = notification;
+                mNotificationPid = callingPid;
+                mNotificationJobStopPolicy = jobEndNotificationPolicy;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     private void doUpdateTransferredNetworkBytes(JobCallback jobCallback, int jobId,
             @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
         // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
@@ -1119,6 +1179,13 @@
                     JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
                     String.valueOf(mRunningJob.getJobId()));
         }
+        if (mNotification != null
+                && mNotificationJobStopPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE) {
+            final String callingPkgName = completedJob.getServiceComponent().getPackageName();
+            mNotificationManagerInternal.cancelNotification(
+                    callingPkgName, callingPkgName, completedJob.getUid(), mNotificationPid,
+                    /* tag */ null, mNotificationId, UserHandle.getUserId(completedJob.getUid()));
+        }
         if (mWakeLock != null) {
             mWakeLock.release();
         }
@@ -1136,6 +1203,7 @@
         mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
         mPendingInternalStopReason = 0;
         mPendingDebugStopReason = null;
+        mNotification = null;
         removeOpTimeOutLocked();
         if (completedJob.isUserVisibleJob()) {
             mService.informObserversOfUserVisibleJobChange(this, completedJob, false);
@@ -1163,6 +1231,7 @@
     private void scheduleOpTimeOutLocked() {
         removeOpTimeOutLocked();
 
+        // TODO(260848384): enforce setNotification timeout for user-initiated jobs
         final long timeoutMillis;
         switch (mVerb) {
             case VERB_EXECUTING:
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 145ac52..a1153e3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -91,8 +91,11 @@
 
     /** Threshold to adjust how often we want to write to the db. */
     private static final long JOB_PERSIST_DELAY = 2000L;
-    private static final String JOB_FILE_SPLIT_PREFIX = "jobs_";
+    @VisibleForTesting
+    static final String JOB_FILE_SPLIT_PREFIX = "jobs_";
     private static final int ALL_UIDS = -1;
+    @VisibleForTesting
+    static final int INVALID_UID = -2;
 
     final Object mLock;
     final Object mWriteScheduleLock;    // used solely for invariants around write scheduling
@@ -529,6 +532,25 @@
         return values;
     }
 
+    @VisibleForTesting
+    static int extractUidFromJobFileName(@NonNull File file) {
+        final String fileName = file.getName();
+        if (fileName.startsWith(JOB_FILE_SPLIT_PREFIX)) {
+            try {
+                final int subEnd = fileName.length() - 4; // -4 for ".xml"
+                final int uid = Integer.parseInt(
+                        fileName.substring(JOB_FILE_SPLIT_PREFIX.length(), subEnd));
+                if (uid < 0) {
+                    return INVALID_UID;
+                }
+                return uid;
+            } catch (Exception e) {
+                Slog.e(TAG, "Unexpected file name format", e);
+            }
+        }
+        return INVALID_UID;
+    }
+
     /**
      * Runnable that writes {@link #mJobSet} out to xml.
      * NOTE: This Runnable locks on mLock
@@ -543,6 +565,42 @@
 
             private void prepare() {
                 mCopyAllJobs = !mUseSplitFiles || mPendingJobWriteUids.get(ALL_UIDS);
+                if (mUseSplitFiles) {
+                    // Put the set of changed UIDs in the copy list so that we update each file,
+                    // especially if we've dropped all jobs for that UID.
+                    if (mPendingJobWriteUids.get(ALL_UIDS)) {
+                        // ALL_UIDS is only used when we switch file splitting policy or for tests,
+                        // so going through the file list here shouldn't be
+                        // a large performance hit on user devices.
+
+                        final File[] files;
+                        try {
+                            files = mJobFileDirectory.listFiles();
+                        } catch (SecurityException e) {
+                            Slog.wtf(TAG, "Not allowed to read job file directory", e);
+                            return;
+                        }
+                        if (files == null) {
+                            Slog.wtfStack(TAG, "Couldn't get job file list");
+                        } else {
+                            for (File file : files) {
+                                final int uid = extractUidFromJobFileName(file);
+                                if (uid != INVALID_UID) {
+                                    mJobStoreCopy.put(uid, new ArrayList<>());
+                                }
+                            }
+                        }
+                    } else {
+                        for (int i = 0; i < mPendingJobWriteUids.size(); ++i) {
+                            mJobStoreCopy.put(mPendingJobWriteUids.keyAt(i), new ArrayList<>());
+                        }
+                    }
+                } else {
+                    // Single file mode.
+                    // Put the catchall UID in the copy list so that we update the single file,
+                    // especially if we've dropped all persisted jobs.
+                    mJobStoreCopy.put(ALL_UIDS, new ArrayList<>());
+                }
             }
 
             @Override
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 49a20650..83b6a8e 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
@@ -33,6 +33,7 @@
 import android.app.AppGlobals;
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
 import android.app.job.JobWorkItem;
 import android.app.job.UserVisibleJobSummary;
 import android.content.ClipData;
@@ -108,6 +109,13 @@
     static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint
     static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint
 
+    private static final int IMPLICIT_CONSTRAINTS = 0
+            | CONSTRAINT_BACKGROUND_NOT_RESTRICTED
+            | CONSTRAINT_DEVICE_NOT_DOZING
+            | CONSTRAINT_FLEXIBLE
+            | CONSTRAINT_TARE_WEALTH
+            | CONSTRAINT_WITHIN_QUOTA;
+
     // The following set of dynamic constraints are for specific use cases (as explained in their
     // relative naming and comments). Right now, they apply different constraints, which is fine,
     // but if in the future, we have overlapping dynamic constraint sets, removing one constraint
@@ -1642,6 +1650,101 @@
         }
     }
 
+    /**
+     * If {@link #isReady()} returns false, this will return a single reason why the job isn't
+     * ready. If {@link #isReady()} returns true, this will return
+     * {@link JobScheduler#PENDING_JOB_REASON_UNDEFINED}.
+     */
+    @JobScheduler.PendingJobReason
+    public int getPendingJobReason() {
+        final int unsatisfiedConstraints = ~satisfiedConstraints
+                & (requiredConstraints | mDynamicConstraints | IMPLICIT_CONSTRAINTS);
+        if ((CONSTRAINT_BACKGROUND_NOT_RESTRICTED & unsatisfiedConstraints) != 0) {
+            // The BACKGROUND_NOT_RESTRICTED constraint could be unsatisfied either because
+            // the app is background restricted, or because we're restricting background work
+            // in battery saver. Assume that background restriction is the reason apps that
+            // jobs are not ready, and battery saver otherwise.
+            // This has the benefit of being consistent for background restricted apps
+            // (they'll always get BACKGROUND_RESTRICTION) as the reason, regardless of
+            // battery saver state.
+            if (mIsUserBgRestricted) {
+                return JobScheduler.PENDING_JOB_REASON_BACKGROUND_RESTRICTION;
+            }
+            return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+        }
+        if ((CONSTRAINT_BATTERY_NOT_LOW & unsatisfiedConstraints) != 0) {
+            if ((CONSTRAINT_BATTERY_NOT_LOW & requiredConstraints) != 0) {
+                // The developer requested this constraint, so it makes sense to return the
+                // explicit constraint reason.
+                return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW;
+            }
+            // Hard-coding right now since the current dynamic constraint sets don't overlap
+            // TODO: return based on active dynamic constraint sets when they start overlapping
+            return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+        }
+        if ((CONSTRAINT_CHARGING & unsatisfiedConstraints) != 0) {
+            if ((CONSTRAINT_CHARGING & requiredConstraints) != 0) {
+                // The developer requested this constraint, so it makes sense to return the
+                // explicit constraint reason.
+                return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CHARGING;
+            }
+            // Hard-coding right now since the current dynamic constraint sets don't overlap
+            // TODO: return based on active dynamic constraint sets when they start overlapping
+            return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+        }
+        if ((CONSTRAINT_CONNECTIVITY & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY;
+        }
+        if ((CONSTRAINT_CONTENT_TRIGGER & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER;
+        }
+        if ((CONSTRAINT_DEVICE_NOT_DOZING & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+        }
+        if ((CONSTRAINT_FLEXIBLE & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION;
+        }
+        if ((CONSTRAINT_IDLE & unsatisfiedConstraints) != 0) {
+            if ((CONSTRAINT_IDLE & requiredConstraints) != 0) {
+                // The developer requested this constraint, so it makes sense to return the
+                // explicit constraint reason.
+                return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE;
+            }
+            // Hard-coding right now since the current dynamic constraint sets don't overlap
+            // TODO: return based on active dynamic constraint sets when they start overlapping
+            return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+        }
+        if ((CONSTRAINT_PREFETCH & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_PREFETCH;
+        }
+        if ((CONSTRAINT_STORAGE_NOT_LOW & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW;
+        }
+        if ((CONSTRAINT_TARE_WEALTH & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_QUOTA;
+        }
+        if ((CONSTRAINT_TIMING_DELAY & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY;
+        }
+        if ((CONSTRAINT_WITHIN_QUOTA & unsatisfiedConstraints) != 0) {
+            return JobScheduler.PENDING_JOB_REASON_QUOTA;
+        }
+
+        if (getEffectiveStandbyBucket() == NEVER_INDEX) {
+            Slog.wtf(TAG, "App in NEVER bucket querying pending job reason");
+            // The user hasn't officially launched this app.
+            return JobScheduler.PENDING_JOB_REASON_USER;
+        }
+        if (serviceProcessName != null) {
+            return JobScheduler.PENDING_JOB_REASON_APP;
+        }
+
+        if (!isReady()) {
+            Slog.wtf(TAG, "Unknown reason job isn't ready");
+        }
+        return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+    }
+
     /** @return whether or not the @param constraint is satisfied */
     public boolean isConstraintSatisfied(int constraint) {
         return (satisfiedConstraints&constraint) != 0;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
index 4067541..7aab67a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
@@ -18,6 +18,7 @@
 
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
 import android.util.IndentingPrintWriter;
 import android.util.proto.ProtoOutputStream;
 
@@ -28,20 +29,23 @@
  * Used by {@link JobSchedulerService} to impose additional restrictions regarding whether jobs
  * should be scheduled or not based on the state of the system/device.
  * Every restriction is associated with exactly one stop reason, which could be retrieved using
- * {@link #getReason()} (and the internal reason via {@link #getInternalReason()}).
+ * {@link #getStopReason()}, one pending reason (retrievable via {@link #getPendingReason()},
+ * (and the internal reason via {@link #getInternalReason()}).
  * Note, that this is not taken into account for the jobs that have
  * {@link JobInfo#BIAS_FOREGROUND_SERVICE} bias or higher.
  */
 public abstract class JobRestriction {
 
     final JobSchedulerService mService;
-    private final int mReason;
+    private final int mStopReason;
+    private final int mPendingReason;
     private final int mInternalReason;
 
-    JobRestriction(JobSchedulerService service, @JobParameters.StopReason int reason,
-            int internalReason) {
+    protected JobRestriction(JobSchedulerService service, @JobParameters.StopReason int stopReason,
+            @JobScheduler.PendingJobReason int pendingReason, int internalReason) {
         mService = service;
-        mReason = reason;
+        mPendingReason = pendingReason;
+        mStopReason = stopReason;
         mInternalReason = internalReason;
     }
 
@@ -70,10 +74,15 @@
     public void dumpConstants(ProtoOutputStream proto) {
     }
 
-    /** @return reason code for the Restriction. */
+    @JobScheduler.PendingJobReason
+    public final int getPendingReason() {
+        return mPendingReason;
+    }
+
+    /** @return stop reason code for the Restriction. */
     @JobParameters.StopReason
-    public final int getReason() {
-        return mReason;
+    public final int getStopReason() {
+        return mStopReason;
     }
 
     public final int getInternalReason() {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
index ca2fd60..830031e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
@@ -18,6 +18,7 @@
 
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
 import android.os.PowerManager;
 import android.os.PowerManager.OnThermalStatusChangedListener;
 import android.util.IndentingPrintWriter;
@@ -42,6 +43,7 @@
 
     public ThermalStatusRestriction(JobSchedulerService service) {
         super(service, JobParameters.STOP_REASON_DEVICE_STATE,
+                JobScheduler.PENDING_JOB_REASON_DEVICE_STATE,
                 JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
     }
 
diff --git a/api/api.go b/api/api.go
index ba0fdc1..c91ff81 100644
--- a/api/api.go
+++ b/api/api.go
@@ -36,6 +36,8 @@
 // built against module_current SDK). Instead they are directly statically
 // linked into the all-framework-module-lib, which is building against hidden
 // APIs.
+// In addition, the modules in this list are allowed to contribute to test APIs
+// stubs.
 var non_updatable_modules = []string{virtualization}
 
 // The intention behind this soong plugin is to generate a number of "merged"
@@ -246,9 +248,33 @@
 }
 
 func createMergedSystemStubs(ctx android.LoadHookContext, modules []string) {
+	// First create the all-updatable-modules-system-stubs
+	{
+		updatable_modules := removeAll(modules, non_updatable_modules)
+		props := libraryProps{}
+		props.Name = proptools.StringPtr("all-updatable-modules-system-stubs")
+		props.Static_libs = transformArray(updatable_modules, "", ".stubs.system")
+		props.Sdk_version = proptools.StringPtr("module_current")
+		props.Visibility = []string{"//frameworks/base"}
+		ctx.CreateModule(java.LibraryFactory, &props)
+	}
+	// Now merge all-updatable-modules-system-stubs and stubs from non-updatable modules
+	// into all-modules-system-stubs.
+	{
+		props := libraryProps{}
+		props.Name = proptools.StringPtr("all-modules-system-stubs")
+		props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.system")
+		props.Static_libs = append(props.Static_libs, "all-updatable-modules-system-stubs")
+		props.Sdk_version = proptools.StringPtr("module_current")
+		props.Visibility = []string{"//frameworks/base"}
+		ctx.CreateModule(java.LibraryFactory, &props)
+	}
+}
+
+func createMergedTestStubsForNonUpdatableModules(ctx android.LoadHookContext) {
 	props := libraryProps{}
-	props.Name = proptools.StringPtr("all-modules-system-stubs")
-	props.Static_libs = transformArray(modules, "", ".stubs.system")
+	props.Name = proptools.StringPtr("all-non-updatable-modules-test-stubs")
+	props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.test")
 	props.Sdk_version = proptools.StringPtr("module_current")
 	props.Visibility = []string{"//frameworks/base"}
 	ctx.CreateModule(java.LibraryFactory, &props)
@@ -360,6 +386,7 @@
 
 	createMergedPublicStubs(ctx, bootclasspath)
 	createMergedSystemStubs(ctx, bootclasspath)
+	createMergedTestStubsForNonUpdatableModules(ctx)
 	createMergedFrameworkModuleLibStubs(ctx, bootclasspath)
 	createMergedFrameworkImpl(ctx, bootclasspath)
 
diff --git a/boot/preloaded-classes b/boot/preloaded-classes
index d8b348e..528ce86 100644
--- a/boot/preloaded-classes
+++ b/boot/preloaded-classes
@@ -9358,8 +9358,8 @@
 android.widget.inline.InlinePresentationSpec$BaseBuilder
 android.widget.inline.InlinePresentationSpec$Builder
 android.widget.inline.InlinePresentationSpec
-android.window.BackEvent$1
-android.window.BackEvent
+android.window.BackMotionEvent$1
+android.window.BackMotionEvent
 android.window.ClientWindowFrames$1
 android.window.ClientWindowFrames
 android.window.CompatOnBackInvokedCallback
diff --git a/config/preloaded-classes b/config/preloaded-classes
index f750249..fa60140 100644
--- a/config/preloaded-classes
+++ b/config/preloaded-classes
@@ -9389,8 +9389,8 @@
 android.widget.inline.InlinePresentationSpec$BaseBuilder
 android.widget.inline.InlinePresentationSpec$Builder
 android.widget.inline.InlinePresentationSpec
-android.window.BackEvent$1
-android.window.BackEvent
+android.window.BackMotionEvent$1
+android.window.BackMotionEvent
 android.window.ClientWindowFrames$1
 android.window.ClientWindowFrames
 android.window.CompatOnBackInvokedCallback
diff --git a/core/api/current.txt b/core/api/current.txt
index e6c0d45..434b60d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -94,6 +94,7 @@
     field public static final String FOREGROUND_SERVICE_CAMERA = "android.permission.FOREGROUND_SERVICE_CAMERA";
     field public static final String FOREGROUND_SERVICE_CONNECTED_DEVICE = "android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE";
     field public static final String FOREGROUND_SERVICE_DATA_SYNC = "android.permission.FOREGROUND_SERVICE_DATA_SYNC";
+    field public static final String FOREGROUND_SERVICE_FILE_MANAGEMENT = "android.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT";
     field public static final String FOREGROUND_SERVICE_HEALTH = "android.permission.FOREGROUND_SERVICE_HEALTH";
     field public static final String FOREGROUND_SERVICE_LOCATION = "android.permission.FOREGROUND_SERVICE_LOCATION";
     field public static final String FOREGROUND_SERVICE_MEDIA_PLAYBACK = "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK";
@@ -8484,7 +8485,26 @@
     method public abstract int enqueue(@NonNull android.app.job.JobInfo, @NonNull android.app.job.JobWorkItem);
     method @NonNull public abstract java.util.List<android.app.job.JobInfo> getAllPendingJobs();
     method @Nullable public abstract android.app.job.JobInfo getPendingJob(int);
+    method public int getPendingJobReason(int);
     method public abstract int schedule(@NonNull android.app.job.JobInfo);
+    field public static final int PENDING_JOB_REASON_APP = 1; // 0x1
+    field public static final int PENDING_JOB_REASON_APP_STANDBY = 2; // 0x2
+    field public static final int PENDING_JOB_REASON_BACKGROUND_RESTRICTION = 3; // 0x3
+    field public static final int PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW = 4; // 0x4
+    field public static final int PENDING_JOB_REASON_CONSTRAINT_CHARGING = 5; // 0x5
+    field public static final int PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY = 6; // 0x6
+    field public static final int PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER = 7; // 0x7
+    field public static final int PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE = 8; // 0x8
+    field public static final int PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY = 9; // 0x9
+    field public static final int PENDING_JOB_REASON_CONSTRAINT_PREFETCH = 10; // 0xa
+    field public static final int PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW = 11; // 0xb
+    field public static final int PENDING_JOB_REASON_DEVICE_STATE = 12; // 0xc
+    field public static final int PENDING_JOB_REASON_EXECUTING = -1; // 0xffffffff
+    field public static final int PENDING_JOB_REASON_INVALID_JOB_ID = -2; // 0xfffffffe
+    field public static final int PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION = 13; // 0xd
+    field public static final int PENDING_JOB_REASON_QUOTA = 14; // 0xe
+    field public static final int PENDING_JOB_REASON_UNDEFINED = 0; // 0x0
+    field public static final int PENDING_JOB_REASON_USER = 15; // 0xf
     field public static final int RESULT_FAILURE = 0; // 0x0
     field public static final int RESULT_SUCCESS = 1; // 0x1
   }
@@ -10346,6 +10366,7 @@
     field @Deprecated @RequiresPermission("android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS") public static final String ACTION_CLOSE_SYSTEM_DIALOGS = "android.intent.action.CLOSE_SYSTEM_DIALOGS";
     field public static final String ACTION_CONFIGURATION_CHANGED = "android.intent.action.CONFIGURATION_CHANGED";
     field public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT";
+    field public static final String ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE";
     field public static final String ACTION_CREATE_REMINDER = "android.intent.action.CREATE_REMINDER";
     field public static final String ACTION_CREATE_SHORTCUT = "android.intent.action.CREATE_SHORTCUT";
     field public static final String ACTION_DATE_CHANGED = "android.intent.action.DATE_CHANGED";
@@ -10594,6 +10615,7 @@
     field public static final String EXTRA_UID = "android.intent.extra.UID";
     field public static final String EXTRA_USER = "android.intent.extra.USER";
     field public static final String EXTRA_USER_INITIATED = "android.intent.extra.USER_INITIATED";
+    field public static final String EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE";
     field public static final int FILL_IN_ACTION = 1; // 0x1
     field public static final int FILL_IN_CATEGORIES = 4; // 0x4
     field public static final int FILL_IN_CLIP_DATA = 128; // 0x80
@@ -11668,7 +11690,7 @@
 
   public class PackageInstaller {
     method public void abandonSession(int);
-    method public void checkInstallConstraints(@NonNull java.util.List<java.lang.String>, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull java.util.function.Consumer<android.content.pm.PackageInstaller.InstallConstraintsResult>);
+    method public void checkInstallConstraints(@NonNull java.util.List<java.lang.String>, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.content.pm.PackageInstaller.InstallConstraintsResult>);
     method public int createSession(@NonNull android.content.pm.PackageInstaller.SessionParams) throws java.io.IOException;
     method @Deprecated @Nullable public android.content.pm.PackageInstaller.SessionInfo getActiveStagedSession();
     method @NonNull public java.util.List<android.content.pm.PackageInstaller.SessionInfo> getActiveStagedSessions();
@@ -12470,6 +12492,7 @@
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10
     field @Deprecated @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1
+    field @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT) public static final int FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT = 4096; // 0x1000
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8
     field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff
@@ -12478,10 +12501,10 @@
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_MICROPHONE}, anyOf={android.Manifest.permission.CAPTURE_AUDIO_OUTPUT, android.Manifest.permission.RECORD_AUDIO}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128; // 0x80
     field @Deprecated public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; // 0x0
     field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL}, anyOf={android.Manifest.permission.MANAGE_OWN_CALLS}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4; // 0x4
-    field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 512; // 0x200
+    field @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING) public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 512; // 0x200
     field public static final int FOREGROUND_SERVICE_TYPE_SHORT_SERVICE = 2048; // 0x800
-    field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1073741824; // 0x40000000
-    field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1024; // 0x400
+    field @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE) public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1073741824; // 0x40000000
+    field @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED) public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1024; // 0x400
     field public int flags;
     field public String permission;
   }
@@ -20179,6 +20202,15 @@
 
 }
 
+package android.location.altitude {
+
+  public final class AltitudeConverter {
+    ctor public AltitudeConverter();
+    method @WorkerThread public void addMslAltitude(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
+  }
+
+}
+
 package android.location.provider {
 
   public final class ProviderProperties implements android.os.Parcelable {
@@ -22017,6 +22049,11 @@
     field public static final int AVCProfileHigh422 = 32; // 0x20
     field public static final int AVCProfileHigh444 = 64; // 0x40
     field public static final int AVCProfileMain = 2; // 0x2
+    field public static final int DTS_HDProfileHRA = 1; // 0x1
+    field public static final int DTS_HDProfileLBR = 2; // 0x2
+    field public static final int DTS_HDProfileMA = 4; // 0x4
+    field public static final int DTS_UHDProfileP1 = 1; // 0x1
+    field public static final int DTS_UHDProfileP2 = 2; // 0x2
     field public static final int DolbyVisionLevel8k30 = 1024; // 0x400
     field public static final int DolbyVisionLevel8k60 = 2048; // 0x800
     field public static final int DolbyVisionLevelFhd24 = 4; // 0x4
@@ -35964,7 +36001,7 @@
     field public static final String ACTION_MANAGE_ALL_SIM_PROFILES_SETTINGS = "android.settings.MANAGE_ALL_SIM_PROFILES_SETTINGS";
     field public static final String ACTION_MANAGE_APPLICATIONS_SETTINGS = "android.settings.MANAGE_APPLICATIONS_SETTINGS";
     field public static final String ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION = "android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION";
-    field public static final String ACTION_MANAGE_APP_LONG_JOBS = "android.settings.MANAGE_APP_LONG_JOBS";
+    field public static final String ACTION_MANAGE_APP_LONG_RUNNING_JOBS = "android.settings.MANAGE_APP_LONG_RUNNING_JOBS";
     field public static final String ACTION_MANAGE_DEFAULT_APPS_SETTINGS = "android.settings.MANAGE_DEFAULT_APPS_SETTINGS";
     field public static final String ACTION_MANAGE_OVERLAY_PERMISSION = "android.settings.action.MANAGE_OVERLAY_PERMISSION";
     field public static final String ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING = "android.settings.MANAGE_SUPERVISOR_RESTRICTED_SETTING";
@@ -38119,6 +38156,7 @@
     field public static final int ERROR_PERMISSION_DENIED = 5; // 0x5
     field public static final int ERROR_UNIMPLEMENTED = 12; // 0xc
     field public static final int ERROR_USER_AUTHENTICATION_REQUIRED = 2; // 0x2
+    field public static final int RETRY_AFTER_NEXT_REBOOT = 4; // 0x4
     field public static final int RETRY_NEVER = 1; // 0x1
     field public static final int RETRY_WHEN_CONNECTIVITY_AVAILABLE = 3; // 0x3
     field public static final int RETRY_WITH_EXPONENTIAL_BACKOFF = 2; // 0x2
@@ -52705,7 +52743,7 @@
     method public android.view.accessibility.AccessibilityNodeInfo getLabeledBy();
     method public int getLiveRegion();
     method public int getMaxTextLength();
-    method public int getMinMillisBetweenContentChanges();
+    method @NonNull public java.time.Duration getMinDurationBetweenContentChanges();
     method public int getMovementGranularities();
     method public CharSequence getPackageName();
     method @Nullable public CharSequence getPaneTitle();
@@ -52794,7 +52832,7 @@
     method public void setLiveRegion(int);
     method public void setLongClickable(boolean);
     method public void setMaxTextLength(int);
-    method public void setMinMillisBetweenContentChanges(int);
+    method public void setMinDurationBetweenContentChanges(@NonNull java.time.Duration);
     method public void setMovementGranularities(int);
     method public void setMultiLine(boolean);
     method public void setPackageName(CharSequence);
@@ -59153,6 +59191,22 @@
 
 package android.window {
 
+  public final class BackEvent {
+    ctor public BackEvent(float, float, float, int);
+    method @FloatRange(from=0, to=1) public float getProgress();
+    method public int getSwipeEdge();
+    method public float getTouchX();
+    method public float getTouchY();
+    field public static final int EDGE_LEFT = 0; // 0x0
+    field public static final int EDGE_RIGHT = 1; // 0x1
+  }
+
+  public interface OnBackAnimationCallback extends android.window.OnBackInvokedCallback {
+    method public default void onBackCancelled();
+    method public default void onBackProgressed(@NonNull android.window.BackEvent);
+    method public default void onBackStarted(@NonNull android.window.BackEvent);
+  }
+
   public interface OnBackInvokedCallback {
     method public void onBackInvoked();
   }
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 286a800..3efb0c6 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -419,10 +419,58 @@
   }
 
   public final class DeviceConfig {
+    field public static final String NAMESPACE_ACTIVITY_MANAGER_COMPONENT_ALIAS = "activity_manager_ca";
     field public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager";
     field public static final String NAMESPACE_APP_CLONING = "app_cloning";
     field public static final String NAMESPACE_APP_STANDBY = "app_standby";
+    field public static final String NAMESPACE_ARC_APP_COMPAT = "arc_app_compat";
+    field public static final String NAMESPACE_CONFIGURATION = "configuration";
+    field public static final String NAMESPACE_CONNECTIVITY_THERMAL_POWER_MANAGER = "connectivity_thermal_power_manager";
+    field public static final String NAMESPACE_CONTACTS_PROVIDER = "contacts_provider";
     field public static final String NAMESPACE_DEVICE_IDLE = "device_idle";
+    field public static final String NAMESPACE_DEVICE_POLICY_MANAGER = "device_policy_manager";
+    field public static final String NAMESPACE_GAME_OVERLAY = "game_overlay";
+    field public static final String NAMESPACE_INTELLIGENCE_CONTENT_SUGGESTIONS = "intelligence_content_suggestions";
+    field public static final String NAMESPACE_INTERACTION_JANK_MONITOR = "interaction_jank_monitor";
+    field public static final String NAMESPACE_LATENCY_TRACKER = "latency_tracker";
+    field public static final String NAMESPACE_MEMORY_SAFETY_NATIVE = "memory_safety_native";
+    field public static final String NAMESPACE_MGLRU_NATIVE = "mglru_native";
+    field public static final String NAMESPACE_REMOTE_KEY_PROVISIONING_NATIVE = "remote_key_provisioning_native";
+    field public static final String NAMESPACE_ROTATION_RESOLVER = "rotation_resolver";
+    field public static final String NAMESPACE_SETTINGS_STATS = "settings_stats";
+    field public static final String NAMESPACE_SETTINGS_UI = "settings_ui";
+    field public static final String NAMESPACE_TARE = "tare";
+    field public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE = "vendor_system_native";
+    field public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT = "vendor_system_native_boot";
+    field public static final String NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE = "virtualization_framework_native";
+    field public static final String NAMESPACE_VOICE_INTERACTION = "voice_interaction";
+    field public static final String NAMESPACE_WEAR = "wear";
+    field public static final String NAMESPACE_WIDGET = "widget";
+    field public static final String NAMESPACE_WINDOW_MANAGER = "window_manager";
+  }
+
+  public static class DeviceConfig.Properties {
+    ctor public DeviceConfig.Properties(@NonNull String, @Nullable java.util.Map<java.lang.String,java.lang.String>);
+  }
+
+  public final class Settings {
+    field public static final int RESET_MODE_PACKAGE_DEFAULTS = 1; // 0x1
+    field public static final int RESET_MODE_TRUSTED_DEFAULTS = 4; // 0x4
+    field public static final int RESET_MODE_UNTRUSTED_CHANGES = 3; // 0x3
+    field public static final int RESET_MODE_UNTRUSTED_DEFAULTS = 2; // 0x2
+  }
+
+  public static final class Settings.Config extends android.provider.Settings.NameValueTable {
+    method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean deleteString(@NonNull String, @NonNull String);
+    method @Nullable @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getString(@NonNull String);
+    method @NonNull @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static java.util.Map<java.lang.String,java.lang.String> getStrings(@NonNull String, @NonNull java.util.List<java.lang.String>);
+    method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static int getSyncDisabledMode();
+    method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean putString(@NonNull String, @NonNull String, @Nullable String, boolean);
+    method public static void registerContentObserver(@Nullable String, boolean, @NonNull android.database.ContentObserver);
+    method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void resetToDefaults(int, @Nullable String);
+    method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean setStrings(@NonNull String, @NonNull java.util.Map<java.lang.String,java.lang.String>) throws android.provider.DeviceConfig.BadConfigException;
+    method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void setSyncDisabledMode(int);
+    method public static void unregisterContentObserver(@NonNull android.database.ContentObserver);
   }
 
   public static final class Settings.Global extends android.provider.Settings.NameValueTable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 3817202..eb2d2ca 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2984,13 +2984,17 @@
   public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable {
     method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method @NonNull public android.content.Context createContext();
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
     method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
     method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @NonNull java.util.List<java.lang.String>, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
+    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.input.VirtualKeyboardConfig);
+    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
+    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
+    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method public int getDeviceId();
     method @Nullable public android.companion.virtual.sensor.VirtualSensor getVirtualSensor(int, @NonNull String);
     method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
@@ -3006,6 +3010,7 @@
     method @NonNull public java.util.Set<android.content.ComponentName> getBlockedCrossTaskNavigations();
     method public int getDefaultActivityPolicy();
     method public int getDefaultNavigationPolicy();
+    method public int getDefaultRecentsPolicy();
     method public int getDevicePolicy(int);
     method public int getLockState();
     method @Nullable public String getName();
@@ -3022,6 +3027,7 @@
     field public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
     field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
+    field public static final int RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS = 1; // 0x1
   }
 
   public static final class VirtualDeviceParams.Builder {
@@ -3032,6 +3038,7 @@
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedActivities(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
+    method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDefaultRecentsPolicy(int);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDevicePolicy(int, int);
     method @NonNull @RequiresPermission(value=android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY, conditional=true) public android.companion.virtual.VirtualDeviceParams.Builder setLockState(int);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setName(@NonNull String);
@@ -3090,7 +3097,7 @@
   public class VirtualSensor {
     method @NonNull public String getName();
     method public int getType();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendSensorEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
   }
 
   public static interface VirtualSensor.SensorStateChangeCallback {
@@ -4637,6 +4644,34 @@
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
   }
 
+  public final class VirtualDpadConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualDpadConfig> CREATOR;
+  }
+
+  public static final class VirtualDpadConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualDpadConfig.Builder> {
+    ctor public VirtualDpadConfig.Builder();
+    method @NonNull public android.hardware.input.VirtualDpadConfig build();
+  }
+
+  public abstract class VirtualInputDeviceConfig {
+    ctor protected VirtualInputDeviceConfig(@NonNull android.hardware.input.VirtualInputDeviceConfig.Builder<? extends android.hardware.input.VirtualInputDeviceConfig.Builder<?>>);
+    ctor protected VirtualInputDeviceConfig(@NonNull android.os.Parcel);
+    method public int getAssociatedDisplayId();
+    method @NonNull public String getInputDeviceName();
+    method public int getProductId();
+    method public int getVendorId();
+  }
+
+  public abstract static class VirtualInputDeviceConfig.Builder<T extends android.hardware.input.VirtualInputDeviceConfig.Builder<T>> {
+    ctor public VirtualInputDeviceConfig.Builder();
+    method @NonNull public T setAssociatedDisplayId(int);
+    method @NonNull public T setInputDeviceName(@NonNull String);
+    method @NonNull public T setProductId(int);
+    method @NonNull public T setVendorId(int);
+  }
+
   public final class VirtualKeyEvent implements android.os.Parcelable {
     method public int describeContents();
     method public int getAction();
@@ -4659,6 +4694,17 @@
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
   }
 
+  public final class VirtualKeyboardConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualKeyboardConfig> CREATOR;
+  }
+
+  public static final class VirtualKeyboardConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualKeyboardConfig.Builder> {
+    ctor public VirtualKeyboardConfig.Builder();
+    method @NonNull public android.hardware.input.VirtualKeyboardConfig build();
+  }
+
   public class VirtualMouse implements java.io.Closeable {
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.graphics.PointF getCursorPosition();
@@ -4689,6 +4735,17 @@
     method @NonNull public android.hardware.input.VirtualMouseButtonEvent.Builder setButtonCode(int);
   }
 
+  public final class VirtualMouseConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualMouseConfig> CREATOR;
+  }
+
+  public static final class VirtualMouseConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualMouseConfig.Builder> {
+    ctor public VirtualMouseConfig.Builder();
+    method @NonNull public android.hardware.input.VirtualMouseConfig build();
+  }
+
   public final class VirtualMouseRelativeEvent implements android.os.Parcelable {
     method public int describeContents();
     method public float getRelativeX();
@@ -4755,6 +4812,21 @@
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
   }
 
+  public final class VirtualTouchscreenConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getHeightInPixels();
+    method public int getWidthInPixels();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualTouchscreenConfig> CREATOR;
+  }
+
+  public static final class VirtualTouchscreenConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualTouchscreenConfig.Builder> {
+    ctor public VirtualTouchscreenConfig.Builder();
+    method @NonNull public android.hardware.input.VirtualTouchscreenConfig build();
+    method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setHeightInPixels(int);
+    method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setWidthInPixels(int);
+  }
+
 }
 
 package android.hardware.lights {
@@ -9350,6 +9422,7 @@
   }
 
   public class WifiNl80211Manager {
+    ctor public WifiNl80211Manager(@NonNull android.content.Context, @NonNull android.os.IBinder);
     method public void abortScan(@NonNull String);
     method public void enableVerboseLogging(boolean);
     method @NonNull public int[] getChannelsMhzForBand(int);
@@ -10634,7 +10707,7 @@
   }
 
   public final class DeviceConfig {
-    method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static void addOnPropertiesChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
+    method public static void addOnPropertiesChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
     method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean deleteProperty(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static boolean getBoolean(@NonNull String, @NonNull String, boolean);
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static float getFloat(@NonNull String, @NonNull String, float);
@@ -10642,17 +10715,22 @@
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static long getLong(@NonNull String, @NonNull String, long);
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static android.provider.DeviceConfig.Properties getProperties(@NonNull String, @NonNull java.lang.String...);
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getProperty(@NonNull String, @NonNull String);
+    method @NonNull public static java.util.List<java.lang.String> getPublicNamespaces();
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getString(@NonNull String, @NonNull String, @Nullable String);
+    method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static int getSyncDisabledMode();
     method public static void removeOnPropertiesChangedListener(@NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
     method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void resetToDefaults(int, @Nullable String);
     method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean setProperties(@NonNull android.provider.DeviceConfig.Properties) throws android.provider.DeviceConfig.BadConfigException;
     method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean setProperty(@NonNull String, @NonNull String, @Nullable String, boolean);
+    method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void setSyncDisabledMode(int);
     field public static final String NAMESPACE_ACTIVITY_MANAGER = "activity_manager";
     field public static final String NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT = "activity_manager_native_boot";
     field public static final String NAMESPACE_ADSERVICES = "adservices";
     field public static final String NAMESPACE_AMBIENT_CONTEXT_MANAGER_SERVICE = "ambient_context_manager_service";
+    field public static final String NAMESPACE_ANDROID = "android";
     field public static final String NAMESPACE_APPSEARCH = "appsearch";
     field public static final String NAMESPACE_APP_COMPAT = "app_compat";
+    field public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
     field public static final String NAMESPACE_APP_HIBERNATION = "app_hibernation";
     field public static final String NAMESPACE_ATTENTION_MANAGER_SERVICE = "attention_manager_service";
     field public static final String NAMESPACE_AUTOFILL = "autofill";
@@ -10664,13 +10742,16 @@
     field public static final String NAMESPACE_CAPTIVEPORTALLOGIN = "captive_portal_login";
     field public static final String NAMESPACE_CLIPBOARD = "clipboard";
     field public static final String NAMESPACE_CONNECTIVITY = "connectivity";
+    field public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
     field public static final String NAMESPACE_CONTENT_CAPTURE = "content_capture";
     field @Deprecated public static final String NAMESPACE_DEX_BOOT = "dex_boot";
     field public static final String NAMESPACE_DISPLAY_MANAGER = "display_manager";
     field public static final String NAMESPACE_GAME_DRIVER = "game_driver";
     field public static final String NAMESPACE_HDMI_CONTROL = "hdmi_control";
+    field public static final String NAMESPACE_INPUT_METHOD_MANAGER = "input_method_manager";
     field public static final String NAMESPACE_INPUT_NATIVE_BOOT = "input_native_boot";
     field public static final String NAMESPACE_INTELLIGENCE_ATTENTION = "intelligence_attention";
+    field public static final String NAMESPACE_JOB_SCHEDULER = "jobscheduler";
     field public static final String NAMESPACE_LMKD_NATIVE = "lmkd_native";
     field public static final String NAMESPACE_LOCATION = "location";
     field public static final String NAMESPACE_MEDIA = "media";
@@ -10692,6 +10773,7 @@
     field public static final String NAMESPACE_RUNTIME_NATIVE_BOOT = "runtime_native_boot";
     field public static final String NAMESPACE_SCHEDULER = "scheduler";
     field public static final String NAMESPACE_SDK_SANDBOX = "sdk_sandbox";
+    field public static final String NAMESPACE_SELECTION_TOOLBAR = "selection_toolbar";
     field public static final String NAMESPACE_STATSD_JAVA = "statsd_java";
     field public static final String NAMESPACE_STATSD_JAVA_BOOT = "statsd_java_boot";
     field public static final String NAMESPACE_STATSD_NATIVE = "statsd_native";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 25c4652..43d4530 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1511,13 +1511,33 @@
     method public static boolean isEncodingLinearPcm(int);
   }
 
+  public final class AudioHalVersionInfo implements java.lang.Comparable<android.media.AudioHalVersionInfo> android.os.Parcelable {
+    method public int compareTo(@NonNull android.media.AudioHalVersionInfo);
+    method public int describeContents();
+    method public int getHalType();
+    method public int getMajorVersion();
+    method public int getMinorVersion();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.media.AudioHalVersionInfo AIDL_1_0;
+    field public static final int AUDIO_HAL_TYPE_AIDL = 1; // 0x1
+    field public static final int AUDIO_HAL_TYPE_HIDL = 0; // 0x0
+    field @NonNull public static final android.os.Parcelable.Creator<android.media.AudioHalVersionInfo> CREATOR;
+    field @NonNull public static final android.media.AudioHalVersionInfo HIDL_2_0;
+    field @NonNull public static final android.media.AudioHalVersionInfo HIDL_4_0;
+    field @NonNull public static final android.media.AudioHalVersionInfo HIDL_5_0;
+    field @NonNull public static final android.media.AudioHalVersionInfo HIDL_6_0;
+    field @NonNull public static final android.media.AudioHalVersionInfo HIDL_7_0;
+    field @NonNull public static final android.media.AudioHalVersionInfo HIDL_7_1;
+    field @NonNull public static final java.util.List<android.media.AudioHalVersionInfo> VERSIONS;
+  }
+
   public class AudioManager {
     method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int abandonAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String);
     method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat);
     method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioTrack getCallUplinkInjectionAudioTrack(@NonNull android.media.AudioFormat);
     method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int);
     method @IntRange(from=0) @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFadeOutDurationOnFocusLossMillis(@NonNull android.media.AudioAttributes);
-    method @Nullable public static String getHalVersion();
+    method @Nullable public static android.media.AudioHalVersionInfo getHalVersion();
     method public static final int[] getPublicStreamTypes();
     method @NonNull public java.util.List<java.lang.Integer> getReportedSurroundFormats();
     method public int getStreamMinVolumeInt(int);
@@ -1719,7 +1739,6 @@
   public class Build {
     method public static boolean is64BitAbi(String);
     method public static boolean isDebuggable();
-    method public static boolean isSecure();
     field public static final boolean IS_EMULATOR;
   }
 
@@ -2191,17 +2210,6 @@
     field public static final android.net.Uri CORP_CONTENT_URI;
   }
 
-  public final class DeviceConfig {
-    field public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager";
-    field public static final String NAMESPACE_ANDROID = "android";
-    field public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
-    field public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
-    field public static final String NAMESPACE_DEVICE_IDLE = "device_idle";
-    field public static final String NAMESPACE_INPUT_METHOD_MANAGER = "input_method_manager";
-    field public static final String NAMESPACE_JOB_SCHEDULER = "jobscheduler";
-    field public static final String NAMESPACE_SELECTION_TOOLBAR = "selection_toolbar";
-  }
-
   public interface InputMethodManagerDeviceConfig {
     field public static final String KEY_HIDE_IME_WHEN_NO_EDITOR_FOCUS = "hide_ime_when_no_editor_focus";
   }
diff --git a/core/java/Android.bp b/core/java/Android.bp
index a4a12d7..128e3de 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -424,6 +424,7 @@
         "android/os/IInterface.java",
         "android/os/Binder.java",
         "android/os/IBinder.java",
+        "android/os/Parcelable.java",
     ],
 }
 
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 2dccd4d..31cbe28 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -6717,6 +6717,13 @@
             ii = null;
         }
 
+        final IActivityManager mgr = ActivityManager.getService();
+        try {
+            mgr.finishAttachApplication(mStartSeq);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+
         final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
         mConfigurationController.updateLocaleListFromAppContext(appContext);
 
@@ -6785,13 +6792,6 @@
         final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
         final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
 
-        final IActivityManager mgr = ActivityManager.getService();
-        try {
-            mgr.finishAttachApplication(mStartSeq);
-        } catch (RemoteException ex) {
-            throw ex.rethrowFromSystemServer();
-        }
-
         // Wait for debugger after we have notified the system to finish attach application
         if (data.debugMode != ApplicationThreadConstants.DEBUG_OFF) {
             // XXX should have option to change the port.
diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java
index 332aadd..e99e360 100644
--- a/core/java/android/app/ForegroundServiceTypePolicy.java
+++ b/core/java/android/app/ForegroundServiceTypePolicy.java
@@ -23,6 +23,7 @@
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
@@ -412,6 +413,22 @@
     );
 
     /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_FILE_MANAGEMENT =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT)
+            }, true),
+            null
+    );
+
+    /**
      * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}.
      *
      * @hide
@@ -1057,6 +1074,8 @@
                     FGS_TYPE_POLICY_SYSTEM_EXEMPTED);
             mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
                     FGS_TYPE_POLICY_SHORT_SERVICE);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT,
+                    FGS_TYPE_POLICY_FILE_MANAGEMENT);
             mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
                     FGS_TYPE_POLICY_SPECIAL_USE);
         }
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index d275c83..32217610 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -305,9 +305,13 @@
             for (int i = mCachedApkAssets.size() - 1; i >= 0; i--) {
                 final ApkKey key = mCachedApkAssets.keyAt(i);
                 if (key.path.equals(path)) {
-                    WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.removeAt(i);
-                    if (apkAssetsRef != null && apkAssetsRef.get() != null) {
-                        apkAssetsRef.get().close();
+                    final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.removeAt(i);
+                    if (apkAssetsRef == null) {
+                        continue;
+                    }
+                    final ApkAssets apkAssets = apkAssetsRef.get();
+                    if (apkAssets != null) {
+                        apkAssets.close();
                     }
                 }
             }
@@ -446,16 +450,14 @@
         ApkAssets apkAssets;
 
         // Optimistically check if this ApkAssets exists somewhere else.
+        final WeakReference<ApkAssets> apkAssetsRef;
         synchronized (mLock) {
-            final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(key);
-            if (apkAssetsRef != null) {
-                apkAssets = apkAssetsRef.get();
-                if (apkAssets != null && apkAssets.isUpToDate()) {
-                    return apkAssets;
-                } else {
-                    // Clean up the reference.
-                    mCachedApkAssets.remove(key);
-                }
+            apkAssetsRef = mCachedApkAssets.get(key);
+        }
+        if (apkAssetsRef != null) {
+            apkAssets = apkAssetsRef.get();
+            if (apkAssets != null && apkAssets.isUpToDate()) {
+                return apkAssets;
             }
         }
 
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 762ac23..3730a6f 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -51,6 +51,8 @@
 import android.app.usage.UsageStatsManager;
 import android.app.wallpapereffectsgeneration.IWallpaperEffectsGenerationManager;
 import android.app.wallpapereffectsgeneration.WallpaperEffectsGenerationManager;
+import android.app.wearable.IWearableSensingManager;
+import android.app.wearable.WearableSensingManager;
 import android.apphibernation.AppHibernationManager;
 import android.appwidget.AppWidgetManager;
 import android.bluetooth.BluetoothFrameworkInitializer;
@@ -1544,6 +1546,17 @@
                                 IAmbientContextManager.Stub.asInterface(iBinder);
                         return new AmbientContextManager(ctx.getOuterContext(), manager);
                     }});
+        registerService(Context.WEARABLE_SENSING_SERVICE, WearableSensingManager.class,
+                new CachedServiceFetcher<WearableSensingManager>() {
+                    @Override
+                    public WearableSensingManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        IBinder iBinder = ServiceManager.getServiceOrThrow(
+                                Context.WEARABLE_SENSING_SERVICE);
+                        IWearableSensingManager manager =
+                                IWearableSensingManager.Stub.asInterface(iBinder);
+                        return new WearableSensingManager(ctx.getOuterContext(), manager);
+                    }});
 
         sInitializing = true;
         try {
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 0837d85..5c47ea2 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -24,11 +24,15 @@
 import android.companion.virtual.sensor.VirtualSensorEvent;
 import android.graphics.Point;
 import android.graphics.PointF;
+import android.hardware.input.VirtualDpadConfig;
+import android.hardware.input.VirtualKeyboardConfig;
 import android.hardware.input.VirtualKeyEvent;
 import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
 import android.hardware.input.VirtualTouchEvent;
+import android.hardware.input.VirtualTouchscreenConfig;
 import android.os.ResultReceiver;
 
 /**
@@ -64,32 +68,22 @@
             IAudioConfigChangedCallback configChangedCallback);
 
     void onAudioSessionEnded();
-
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
     void createVirtualDpad(
-            int displayId,
-            String inputDeviceName,
-            int vendorId,
-            int productId,
+            in VirtualDpadConfig config,
             IBinder token);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
     void createVirtualKeyboard(
-            int displayId,
-            String inputDeviceName,
-            int vendorId,
-            int productId,
+            in VirtualKeyboardConfig config,
             IBinder token);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
     void createVirtualMouse(
-            int displayId,
-            String inputDeviceName,
-            int vendorId,
-            int productId,
+            in VirtualMouseConfig config,
             IBinder token);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
     void createVirtualTouchscreen(
-            int displayId,
-            String inputDeviceName,
-            int vendorId,
-            int productId,
-            IBinder token,
-            in Point screenSize);
+            in VirtualTouchscreenConfig config,
+            IBinder token);
     void unregisterInputDevice(IBinder token);
     int getInputDeviceId(IBinder token);
     boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 568185a..0e6cfb1 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -41,9 +41,13 @@
 import android.hardware.display.VirtualDisplay;
 import android.hardware.display.VirtualDisplayConfig;
 import android.hardware.input.VirtualDpad;
+import android.hardware.input.VirtualDpadConfig;
 import android.hardware.input.VirtualKeyboard;
+import android.hardware.input.VirtualKeyboardConfig;
 import android.hardware.input.VirtualMouse;
+import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualTouchscreen;
+import android.hardware.input.VirtualTouchscreenConfig;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -71,7 +75,6 @@
 @SystemService(Context.VIRTUAL_DEVICE_SERVICE)
 public final class VirtualDeviceManager {
 
-    private static final boolean DEBUG = false;
     private static final String TAG = "VirtualDeviceManager";
 
     private static final int DEFAULT_VIRTUAL_DISPLAY_FLAGS =
@@ -80,7 +83,6 @@
                     | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
                     | DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL
                     | DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
-                    | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
                     | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_FOCUS;
 
     /**
@@ -310,6 +312,18 @@
         }
 
         /**
+         * @return A new Context bound to this device. This is a convenience method equivalent to
+         * calling {@link Context#createDeviceContext(int)} with the device id of this device.
+         */
+        public @NonNull Context createContext() {
+            try {
+                return mContext.createDeviceContext(mVirtualDevice.getDeviceId());
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Returns this device's sensor with the given type and name, if any.
          *
          * @see VirtualDeviceParams.Builder#addVirtualSensorConfig
@@ -500,23 +514,15 @@
         /**
          * Creates a virtual dpad.
          *
-         * @param display the display that the events inputted through this device should target
-         * @param inputDeviceName the name to call this input device
-         * @param vendorId the PCI vendor id
-         * @param productId the product id, as defined by the vendor
+         * @param config the configurations of the virtual Dpad.
          */
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
-        public VirtualDpad createVirtualDpad(
-                @NonNull VirtualDisplay display,
-                @NonNull String inputDeviceName,
-                int vendorId,
-                int productId) {
+        public VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
             try {
                 final IBinder token = new Binder(
-                        "android.hardware.input.VirtualDpad:" + inputDeviceName);
-                mVirtualDevice.createVirtualDpad(display.getDisplay().getDisplayId(),
-                        inputDeviceName, vendorId, productId, token);
+                        "android.hardware.input.VirtualDpad:" + config.getInputDeviceName());
+                mVirtualDevice.createVirtualDpad(config, token);
                 return new VirtualDpad(mVirtualDevice, token);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
@@ -526,23 +532,15 @@
         /**
          * Creates a virtual keyboard.
          *
-         * @param display the display that the events inputted through this device should target
-         * @param inputDeviceName the name to call this input device
-         * @param vendorId the PCI vendor id
-         * @param productId the product id, as defined by the vendor
+         * @param config the configurations of the virtual keyboard.
          */
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
-        public VirtualKeyboard createVirtualKeyboard(
-                @NonNull VirtualDisplay display,
-                @NonNull String inputDeviceName,
-                int vendorId,
-                int productId) {
+        public VirtualKeyboard createVirtualKeyboard(@NonNull VirtualKeyboardConfig config) {
             try {
                 final IBinder token = new Binder(
-                        "android.hardware.input.VirtualKeyboard:" + inputDeviceName);
-                mVirtualDevice.createVirtualKeyboard(display.getDisplay().getDisplayId(),
-                        inputDeviceName, vendorId, productId, token);
+                        "android.hardware.input.VirtualKeyboard:" + config.getInputDeviceName());
+                mVirtualDevice.createVirtualKeyboard(config, token);
                 return new VirtualKeyboard(mVirtualDevice, token);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
@@ -550,26 +548,90 @@
         }
 
         /**
+         * Creates a virtual keyboard.
+         *
+         * @param display         the display that the events inputted through this device should
+         *                        target
+         * @param inputDeviceName the name to call this input device
+         * @param vendorId        the PCI vendor id
+         * @param productId       the product id, as defined by the vendor
+         * @see #createVirtualKeyboard(VirtualKeyboardConfig config)
+         * @deprecated Use {@link #createVirtualKeyboard(VirtualKeyboardConfig config)} instead
+         */
+        @Deprecated
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        public VirtualKeyboard createVirtualKeyboard(@NonNull VirtualDisplay display,
+                @NonNull String inputDeviceName, int vendorId, int productId) {
+            VirtualKeyboardConfig keyboardConfig =
+                    new VirtualKeyboardConfig.Builder()
+                            .setVendorId(vendorId)
+                            .setProductId(productId)
+                            .setInputDeviceName(inputDeviceName)
+                            .setAssociatedDisplayId(display.getDisplay().getDisplayId())
+                            .build();
+            return createVirtualKeyboard(keyboardConfig);
+        }
+
+        /**
+         * Creates a virtual mouse.
+         *
+         * @param config the configurations of the virtual mouse.
+         */
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        public VirtualMouse createVirtualMouse(@NonNull VirtualMouseConfig config) {
+            try {
+                final IBinder token = new Binder(
+                        "android.hardware.input.VirtualMouse:" + config.getInputDeviceName());
+                mVirtualDevice.createVirtualMouse(config, token);
+                return new VirtualMouse(mVirtualDevice, token);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Creates a virtual mouse.
          *
-         * @param display the display that the events inputted through this device should target
+         * @param display         the display that the events inputted through this device should
+         *                        target
          * @param inputDeviceName the name to call this input device
-         * @param vendorId the PCI vendor id
-         * @param productId the product id, as defined by the vendor
+         * @param vendorId        the PCI vendor id
+         * @param productId       the product id, as defined by the vendor
+         * @see #createVirtualMouse(VirtualMouseConfig config)
+         * @deprecated Use {@link #createVirtualMouse(VirtualMouseConfig config)} instead
+         * *
+         */
+        @Deprecated
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        public VirtualMouse createVirtualMouse(@NonNull VirtualDisplay display,
+                @NonNull String inputDeviceName, int vendorId, int productId) {
+            VirtualMouseConfig mouseConfig =
+                    new VirtualMouseConfig.Builder()
+                            .setVendorId(vendorId)
+                            .setProductId(productId)
+                            .setInputDeviceName(inputDeviceName)
+                            .setAssociatedDisplayId(display.getDisplay().getDisplayId())
+                            .build();
+            return createVirtualMouse(mouseConfig);
+        }
+
+        /**
+         * Creates a virtual touchscreen.
+         *
+         * @param config the configurations of the virtual touchscreen.
          */
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
-        public VirtualMouse createVirtualMouse(
-                @NonNull VirtualDisplay display,
-                @NonNull String inputDeviceName,
-                int vendorId,
-                int productId) {
+        public VirtualTouchscreen createVirtualTouchscreen(
+                @NonNull VirtualTouchscreenConfig config) {
             try {
                 final IBinder token = new Binder(
-                        "android.hardware.input.VirtualMouse:" + inputDeviceName);
-                mVirtualDevice.createVirtualMouse(display.getDisplay().getDisplayId(),
-                        inputDeviceName, vendorId, productId, token);
-                return new VirtualMouse(mVirtualDevice, token);
+                        "android.hardware.input.VirtualTouchscreen:" + config.getInputDeviceName());
+                mVirtualDevice.createVirtualTouchscreen(config, token);
+                return new VirtualTouchscreen(mVirtualDevice, token);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -578,29 +640,32 @@
         /**
          * Creates a virtual touchscreen.
          *
-         * @param display the display that the events inputted through this device should target
+         * @param display         the display that the events inputted through this device should
+         *                        target
          * @param inputDeviceName the name to call this input device
-         * @param vendorId the PCI vendor id
-         * @param productId the product id, as defined by the vendor
+         * @param vendorId        the PCI vendor id
+         * @param productId       the product id, as defined by the vendor
+         * @see #createVirtualTouchscreen(VirtualTouchscreenConfig config)
+         * @deprecated Use {@link #createVirtualTouchscreen(VirtualTouchscreenConfig config)}
+         * instead
          */
+        @Deprecated
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
-        public VirtualTouchscreen createVirtualTouchscreen(
-                @NonNull VirtualDisplay display,
-                @NonNull String inputDeviceName,
-                int vendorId,
-                int productId) {
-            try {
-                final IBinder token = new Binder(
-                        "android.hardware.input.VirtualTouchscreen:" + inputDeviceName);
-                final Point size = new Point();
-                display.getDisplay().getSize(size);
-                mVirtualDevice.createVirtualTouchscreen(display.getDisplay().getDisplayId(),
-                        inputDeviceName, vendorId, productId, token, size);
-                return new VirtualTouchscreen(mVirtualDevice, token);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
+        public VirtualTouchscreen createVirtualTouchscreen(@NonNull VirtualDisplay display,
+                @NonNull String inputDeviceName, int vendorId, int productId) {
+            final Point size = new Point();
+            display.getDisplay().getSize(size);
+            VirtualTouchscreenConfig touchscreenConfig =
+                    new VirtualTouchscreenConfig.Builder()
+                            .setVendorId(vendorId)
+                            .setProductId(productId)
+                            .setInputDeviceName(inputDeviceName)
+                            .setAssociatedDisplayId(display.getDisplay().getDisplayId())
+                            .setWidthInPixels(size.x)
+                            .setHeightInPixels(size.y)
+                            .build();
+            return createVirtualTouchscreen(touchscreenConfig);
         }
 
         /**
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 1cbe910..be77f2b 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -147,6 +147,19 @@
      */
     public static final int POLICY_TYPE_SENSORS = 0;
 
+    /** @hide */
+    @IntDef(flag = true, prefix = "RECENTS_POLICY_",
+            value = {RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS})
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+    public @interface RecentsPolicy {}
+
+    /**
+     * If set, activities launched on this virtual device are allowed to appear in the host device
+     * of the recently launched activities list.
+     */
+    public static final int RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS = 1 << 0;
+
     private final int mLockState;
     @NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
     @NonNull private final ArraySet<ComponentName> mAllowedCrossTaskNavigations;
@@ -161,6 +174,8 @@
     // Mapping of @PolicyType to @DevicePolicy
     @NonNull private final SparseIntArray mDevicePolicies;
     @NonNull private final List<VirtualSensorConfig> mVirtualSensorConfigs;
+    @RecentsPolicy
+    private final int mDefaultRecentsPolicy;
 
     private VirtualDeviceParams(
             @LockState int lockState,
@@ -173,7 +188,8 @@
             @ActivityPolicy int defaultActivityPolicy,
             @Nullable String name,
             @NonNull SparseIntArray devicePolicies,
-            @NonNull List<VirtualSensorConfig> virtualSensorConfigs) {
+            @NonNull List<VirtualSensorConfig> virtualSensorConfigs,
+            @RecentsPolicy int defaultRecentsPolicy) {
         mLockState = lockState;
         mUsersWithMatchingAccounts =
                 new ArraySet<>(Objects.requireNonNull(usersWithMatchingAccounts));
@@ -188,6 +204,7 @@
         mName = name;
         mDevicePolicies = Objects.requireNonNull(devicePolicies);
         mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs);
+        mDefaultRecentsPolicy = defaultRecentsPolicy;
     }
 
     @SuppressWarnings("unchecked")
@@ -204,6 +221,7 @@
         mDevicePolicies = parcel.readSparseIntArray();
         mVirtualSensorConfigs = new ArrayList<>();
         parcel.readTypedList(mVirtualSensorConfigs, VirtualSensorConfig.CREATOR);
+        mDefaultRecentsPolicy = parcel.readInt();
     }
 
     /**
@@ -328,6 +346,16 @@
         return mVirtualSensorConfigs;
     }
 
+    /**
+     * Returns the policy of how to handle activities in recents.
+     *
+     * @see RecentsPolicy
+     */
+    @RecentsPolicy
+    public int getDefaultRecentsPolicy() {
+        return mDefaultRecentsPolicy;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -346,6 +374,7 @@
         dest.writeString8(mName);
         dest.writeSparseIntArray(mDevicePolicies);
         dest.writeTypedList(mVirtualSensorConfigs);
+        dest.writeInt(mDefaultRecentsPolicy);
     }
 
     @Override
@@ -377,15 +406,17 @@
                 && Objects.equals(mAllowedActivities, that.mAllowedActivities)
                 && Objects.equals(mBlockedActivities, that.mBlockedActivities)
                 && mDefaultActivityPolicy == that.mDefaultActivityPolicy
-                && Objects.equals(mName, that.mName);
+                && Objects.equals(mName, that.mName)
+                && mDefaultRecentsPolicy == that.mDefaultRecentsPolicy;
     }
 
     @Override
     public int hashCode() {
         int hashCode = Objects.hash(
                 mLockState, mUsersWithMatchingAccounts, mAllowedCrossTaskNavigations,
-                mBlockedCrossTaskNavigations, mDefaultNavigationPolicy,  mAllowedActivities,
-                mBlockedActivities, mDefaultActivityPolicy, mName, mDevicePolicies);
+                mBlockedCrossTaskNavigations, mDefaultNavigationPolicy, mAllowedActivities,
+                mBlockedActivities, mDefaultActivityPolicy, mName, mDevicePolicies,
+                mDefaultRecentsPolicy);
         for (int i = 0; i < mDevicePolicies.size(); i++) {
             hashCode = 31 * hashCode + mDevicePolicies.keyAt(i);
             hashCode = 31 * hashCode + mDevicePolicies.valueAt(i);
@@ -407,6 +438,7 @@
                 + " mDefaultActivityPolicy=" + mDefaultActivityPolicy
                 + " mName=" + mName
                 + " mDevicePolicies=" + mDevicePolicies
+                + " mDefaultRecentsPolicy=" + mDefaultRecentsPolicy
                 + ")";
     }
 
@@ -442,6 +474,7 @@
         @Nullable private String mName;
         @NonNull private SparseIntArray mDevicePolicies = new SparseIntArray();
         @NonNull private List<VirtualSensorConfig> mVirtualSensorConfigs = new ArrayList<>();
+        private int mDefaultRecentsPolicy;
 
         /**
          * Sets the lock state of the device. The permission {@code ADD_ALWAYS_UNLOCKED_DISPLAY}
@@ -647,6 +680,17 @@
         }
 
         /**
+         * Sets the policy to indicate how activities are handled in recents.
+         *
+         * @param defaultRecentsPolicy A policy specifying how to handle activities in recents.
+         */
+        @NonNull
+        public Builder setDefaultRecentsPolicy(@RecentsPolicy int defaultRecentsPolicy) {
+            mDefaultRecentsPolicy = defaultRecentsPolicy;
+            return this;
+        }
+
+        /**
          * Builds the {@link VirtualDeviceParams} instance.
          *
          * @throws IllegalArgumentException if there's mismatch between policy definition and
@@ -684,7 +728,8 @@
                     mDefaultActivityPolicy,
                     mName,
                     mDevicePolicies,
-                    mVirtualSensorConfigs);
+                    mVirtualSensorConfigs,
+                    mDefaultRecentsPolicy);
         }
     }
 }
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensor.java b/core/java/android/companion/virtual/sensor/VirtualSensor.java
index a184481..58a5387 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensor.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensor.java
@@ -20,6 +20,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.companion.virtual.IVirtualDevice;
+import android.hardware.Sensor;
 import android.os.IBinder;
 import android.os.RemoteException;
 
@@ -68,8 +69,10 @@
     }
 
     /**
-     * Returns the
-     * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+     * Returns the type of the sensor.
+     *
+     * @see Sensor#getType()
+     * @see <a href="https://source.android.com/devices/sensors/sensor-types">Sensor types</a>
      */
     public int getType() {
         return mType;
@@ -87,7 +90,7 @@
      * Send a sensor event to the system.
      */
     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void sendSensorEvent(@NonNull VirtualSensorEvent event) {
+    public void sendEvent(@NonNull VirtualSensorEvent event) {
         try {
             mVirtualDevice.sendSensorEvent(mToken, event);
         } catch (RemoteException e) {
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
index 7982fa5..eb2f9dd 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorConfig.java
@@ -23,6 +23,7 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.hardware.Sensor;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -77,8 +78,10 @@
     }
 
     /**
-     * Returns the
-     * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+     * Returns the type of the sensor.
+     *
+     * @see Sensor#getType()
+     * @see <a href="https://source.android.com/devices/sensors/sensor-types">Sensor types</a>
      */
     public int getType() {
         return mType;
@@ -150,8 +153,7 @@
         /**
          * Creates a new builder.
          *
-         * @param type The
-         * <a href="https://source.android.com/devices/sensors/sensor-types">type</a> of the sensor.
+         * @param type The type of the sensor, matching {@link Sensor#getType}.
          * @param name The name of the sensor. Must be unique among all sensors with the same type
          * that belong to the same virtual device.
          */
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java
index 8f8860e..01b4975 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensorEvent.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.hardware.Sensor;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
@@ -60,9 +61,11 @@
     }
 
     /**
-     * Returns the values of this sensor event. The length and contents depend on the
-     * <a href="https://source.android.com/devices/sensors/sensor-types">sensor type</a>.
+     * Returns the values of this sensor event. The length and contents depend on the sensor type.
+     *
      * @see android.hardware.SensorEvent#values
+     * @see Sensor#getType()
+     * @see <a href="https://source.android.com/devices/sensors/sensor-types">Sensor types</a>
      */
     @NonNull
     public float[] getValues() {
@@ -90,7 +93,9 @@
 
         /**
          * Creates a new builder.
-         * @param values the values of the sensor event. @see android.hardware.SensorEvent#values
+         *
+         * @param values the values of the sensor event.
+         * @see android.hardware.SensorEvent#values
          */
         public Builder(@NonNull float[] values) {
             mValues = values;
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 65b3ca4..86a672f 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5107,6 +5107,21 @@
     public static final String ACTION_VIEW_LOCUS = "android.intent.action.VIEW_LOCUS";
 
     /**
+     * Activity Action: Starts a note-taking activity that can be used to create a note. This action
+     * can be used to start an activity on the lock screen. Activity should ensure to appropriately
+     * handle privacy sensitive data and features when launched on the lock screen. See
+     * {@link android.app.KeyguardManager} for lock screen checks.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE";
+
+    /**
+     * A boolean extra used with {@link #ACTION_CREATE_NOTE} indicating whether the launched
+     * note-taking activity should show a UI that is suitable to use with stylus input.
+     */
+    public static final String EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE";
+
+    /**
      * Broadcast Action: Sent to the integrity component when a package
      * needs to be verified. The data contains the package URI along with other relevant
      * information.
@@ -6109,6 +6124,14 @@
     public static final String EXTRA_REPLACING = "android.intent.extra.REPLACING";
 
     /**
+     * Used as a boolean extra field in {@link android.content.Intent#ACTION_PACKAGE_REMOVED}
+     * intents to indicate that this is a system update uninstall.
+     * @hide
+     */
+    public static final String EXTRA_SYSTEM_UPDATE_UNINSTALL =
+            "android.intent.extra.SYSTEM_UPDATE_UNINSTALL";
+
+    /**
      * Used as an int extra field in {@link android.app.AlarmManager} pending intents
      * to tell the application being invoked how many pending alarms are being
      * delivered with the intent.  For one-shot alarms this will always be 1.
diff --git a/core/java/android/content/om/OverlayInfo.java b/core/java/android/content/om/OverlayInfo.java
index c66f49c..a470de2 100644
--- a/core/java/android/content/om/OverlayInfo.java
+++ b/core/java/android/content/om/OverlayInfo.java
@@ -51,6 +51,7 @@
             STATE_ENABLED_IMMUTABLE,
             // @Deprecated STATE_TARGET_IS_BEING_REPLACED,
             STATE_OVERLAY_IS_BEING_REPLACED,
+            STATE_SYSTEM_UPDATE_UNINSTALL,
     })
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -128,6 +129,14 @@
     public static final int STATE_ENABLED_IMMUTABLE = 6;
 
     /**
+     * The target package needs to be refreshed as a result of a system update uninstall, which
+     * must recalculate the state of overlays against the newly enabled system package, which may
+     * differ in resources/policy from the /data variant that was uninstalled.
+     * @hide
+     */
+    public static final int STATE_SYSTEM_UPDATE_UNINSTALL = 7;
+
+    /**
      * Overlay category: theme.
      * <p>
      * Change how Android (including the status bar, dialogs, ...) looks.
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index c79f99d..1f01ae9 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -867,10 +867,15 @@
      */
     public void checkInstallConstraints(@NonNull List<String> packageNames,
             @NonNull InstallConstraints constraints,
+            @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<InstallConstraintsResult> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
         try {
             var remoteCallback = new RemoteCallback(b -> {
-                callback.accept(b.getParcelable("result", InstallConstraintsResult.class));
+                executor.execute(() -> {
+                    callback.accept(b.getParcelable("result", InstallConstraintsResult.class));
+                });
             });
             mInstaller.checkInstallConstraints(
                     mInstallerPackageName, packageNames, constraints, remoteCallback);
@@ -3675,7 +3680,7 @@
     }
 
     /**
-     * The callback result of {@link #checkInstallConstraints(List, InstallConstraints, Consumer)}.
+     * The callback result of {@link #checkInstallConstraints(List, InstallConstraints, Executor, Consumer)}.
      */
     @DataClass(genParcelable = true, genHiddenConstructor = true)
     public static final class InstallConstraintsResult implements Parcelable {
@@ -3783,7 +3788,7 @@
     /**
      * A class to encapsulate constraints for installation.
      *
-     * When used with {@link #checkInstallConstraints(List, InstallConstraints, Consumer)}, it
+     * When used with {@link #checkInstallConstraints(List, InstallConstraints, Executor, Consumer)}, it
      * specifies the conditions to check against for the packages in question. This can be used
      * by app stores to deliver auto updates without disrupting the user experience (referred as
      * gentle update) - for example, an app store might hold off updates when it find out the
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 7ea6733..fc2c532 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -331,8 +331,7 @@
      * Messaging use cases which host local server to relay messages across devices.
      */
     @RequiresPermission(
-            value = Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING,
-            conditional = true
+            value = Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING
     )
     public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 1 << 9;
 
@@ -360,8 +359,7 @@
      * </p>
      */
     @RequiresPermission(
-            value = Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED,
-            conditional = true
+            value = Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED
     )
     public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1 << 10;
 
@@ -412,6 +410,17 @@
     public static final int FOREGROUND_SERVICE_TYPE_SHORT_SERVICE = 1 << 11;
 
     /**
+     * Constant corresponding to {@code fileManagement} in
+     * the {@link android.R.attr#foregroundServiceType} attribute.
+     * The file management use case which manages files/directories, often involving file I/O
+     * across the file system.
+     */
+    @RequiresPermission(
+            value = Manifest.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT
+    )
+    public static final int FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT = 1 << 12;
+
+    /**
      * Constant corresponding to {@code specialUse} in
      * the {@link android.R.attr#foregroundServiceType} attribute.
      * Use cases that can't be categorized into any other foreground service types, but also
@@ -457,8 +466,7 @@
      * </pre>
      */
     @RequiresPermission(
-            value = Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE,
-            conditional = true
+            value = Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE
     )
     public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1 << 30;
 
@@ -495,7 +503,8 @@
             FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING,
             FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
             FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
-            FOREGROUND_SERVICE_TYPE_SPECIAL_USE
+            FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT,
+            FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ForegroundServiceType {}
@@ -579,6 +588,8 @@
                 return "systemExempted";
             case FOREGROUND_SERVICE_TYPE_SHORT_SERVICE:
                 return "shortService";
+            case FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT:
+                return "fileManagement";
             case FOREGROUND_SERVICE_TYPE_SPECIAL_USE:
                 return "specialUse";
             default:
diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index 1c1f58a..45e0efb 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -43,6 +43,36 @@
       "name": "ApkVerityTest"
     },
     {
+      "name": "CtsAppFgsTestCases",
+      "file_patterns": ["(/|^)ServiceInfo[^/]*"],
+      "options": [
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
+      "name": "CtsShortFgsTestCases",
+      "file_patterns": ["(/|^)ServiceInfo[^/]*"],
+      "options": [
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
       "name": "CtsIncrementalInstallHostTestCases",
       "options": [
         {
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index f7675e8..23d108f 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -419,6 +419,15 @@
     @TestApi
     public static final int VIRTUAL_DISPLAY_FLAG_OWN_FOCUS = 1 << 14;
 
+    /**
+     * Virtual display flags: Indicates that the display should not be a part of the default
+     * DisplayGroup and instead be part of a DisplayGroup associated with its virtual device.
+     *
+     * @see #createVirtualDisplay
+     * @hide
+     */
+    public static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15;
+
 
     /** @hide */
     @IntDef(prefix = {"MATCH_CONTENT_FRAMERATE_"}, value = {
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 6f63dbf..a748b60 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -1014,8 +1014,22 @@
             return;
         }
 
-        // TODO(b/218388821): Propagate all the parameters to FingerprintService.
-        Slog.e(TAG, "onPointerDown: not implemented!");
+        final PointerContext pc = new PointerContext();
+        pc.pointerId = pointerId;
+        pc.x = x;
+        pc.y = y;
+        pc.minor = minor;
+        pc.major = major;
+        pc.orientation = orientation;
+        pc.time = time;
+        pc.gestureStart = gestureStart;
+        pc.isAod = isAod;
+
+        try {
+            mService.onPointerDown(requestId, sensorId, pc);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -1040,8 +1054,22 @@
             return;
         }
 
-        // TODO(b/218388821): Propagate all the parameters to FingerprintService.
-        Slog.e(TAG, "onPointerUp: not implemented!");
+        final PointerContext pc = new PointerContext();
+        pc.pointerId = pointerId;
+        pc.x = x;
+        pc.y = y;
+        pc.minor = minor;
+        pc.major = major;
+        pc.orientation = orientation;
+        pc.time = time;
+        pc.gestureStart = gestureStart;
+        pc.isAod = isAod;
+
+        try {
+            mService.onPointerUp(requestId, sensorId, pc);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index bdcbcaa..b26c0a2 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -32,6 +32,8 @@
 import android.os.IBinder;
 import android.os.IVibratorStateListener;
 import android.os.VibrationEffect;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.InputMonitor;
@@ -107,6 +109,20 @@
     void removeKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier,
             String keyboardLayoutDescriptor);
 
+    // New Keyboard layout config APIs
+    String getKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier, int userId,
+            in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype);
+
+    @EnforcePermission("SET_KEYBOARD_LAYOUT")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.SET_KEYBOARD_LAYOUT)")
+    void setKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier, int userId,
+            in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype,
+            String keyboardLayoutDescriptor);
+
+    String[] getKeyboardLayoutListForInputDevice(in InputDeviceIdentifier identifier, int userId,
+            in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype);
+
     // Registers an input devices changed listener.
     void registerInputDevicesChangedListener(IInputDevicesChangedListener listener);
 
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index a157a8f..cea3fa1 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -26,6 +26,7 @@
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
+import android.annotation.UserIdInt;
 import android.app.ActivityThread;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -66,6 +67,8 @@
 import android.view.PointerIcon;
 import android.view.VerifiedInputEvent;
 import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -889,6 +892,91 @@
     }
 
     /**
+     * Gets the keyboard layout descriptor for the specified input device, userId, imeInfo and
+     * imeSubtype.
+     *
+     * @param identifier Identifier for the input device
+     * @param userId user profile ID
+     * @param imeInfo contains IME information like imeId, etc.
+     * @param imeSubtype contains IME subtype information like input languageTag, layoutType, etc.
+     * @return The keyboard layout descriptor, or null if no keyboard layout has been set.
+     *
+     * @hide
+     */
+    @Nullable
+    public String getKeyboardLayoutForInputDevice(@NonNull InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype) {
+        try {
+            return mIm.getKeyboardLayoutForInputDevice(identifier, userId, imeInfo, imeSubtype);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sets the keyboard layout descriptor for the specified input device, userId, imeInfo and
+     * imeSubtype.
+     *
+     * <p>
+     * This method may have the side-effect of causing the input device in question to be
+     * reconfigured.
+     * </p>
+     *
+     * @param identifier The identifier for the input device.
+     * @param userId user profile ID
+     * @param imeInfo contains IME information like imeId, etc.
+     * @param imeSubtype contains IME subtype information like input languageTag, layoutType, etc.
+     * @param keyboardLayoutDescriptor The keyboard layout descriptor to use, must not be null.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.SET_KEYBOARD_LAYOUT)
+    public void setKeyboardLayoutForInputDevice(@NonNull InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype, @NonNull String keyboardLayoutDescriptor) {
+        if (identifier == null) {
+            throw new IllegalArgumentException("identifier must not be null");
+        }
+        if (keyboardLayoutDescriptor == null) {
+            throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null");
+        }
+
+        try {
+            mIm.setKeyboardLayoutForInputDevice(identifier, userId, imeInfo, imeSubtype,
+                    keyboardLayoutDescriptor);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Gets all keyboard layout descriptors that are enabled for the specified input device, userId,
+     * imeInfo and imeSubtype.
+     *
+     * @param identifier The identifier for the input device.
+     * @param userId user profile ID
+     * @param imeInfo contains IME information like imeId, etc.
+     * @param imeSubtype contains IME subtype information like input languageTag, layoutType, etc.
+     * @return The keyboard layout descriptors.
+     *
+     * @hide
+     */
+    public String[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype) {
+        if (identifier == null) {
+            throw new IllegalArgumentException("inputDeviceDescriptor must not be null");
+        }
+
+        try {
+            return mIm.getKeyboardLayoutListForInputDevice(identifier, userId, imeInfo, imeSubtype);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Gets the mouse pointer speed.
      * <p>
      * Only returns the permanent mouse pointer speed.  Ignores any temporary pointer
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index 4d61553..8133472 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -32,8 +32,8 @@
 /**
  * A virtual dpad representing a key input mechanism on a remote device.
  *
- * This registers an InputDevice that is interpreted like a physically-connected device and
- * dispatches received events to it.
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.</p>
  *
  * @hide
  */
@@ -44,6 +44,7 @@
             Collections.unmodifiableSet(
                     new HashSet<>(
                             Arrays.asList(
+                                    KeyEvent.KEYCODE_BACK,
                                     KeyEvent.KEYCODE_DPAD_UP,
                                     KeyEvent.KEYCODE_DPAD_DOWN,
                                     KeyEvent.KEYCODE_DPAD_LEFT,
@@ -58,8 +59,15 @@
     /**
      * Sends a key event to the system.
      *
-     * Supported key codes are KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT,
-     * KEYCODE_DPAD_RIGHT and KEYCODE_DPAD_CENTER,
+     * <p>Supported key codes are:
+     * <ul>
+     *     <li>{@link KeyEvent.KEYCODE_DPAD_UP}</li>
+     *     <li>{@link KeyEvent.KEYCODE_DPAD_DOWN}</li>
+     *     <li>{@link KeyEvent.KEYCODE_DPAD_LEFT}</li>
+     *     <li>{@link KeyEvent.KEYCODE_DPAD_RIGHT}</li>
+     *     <li>{@link KeyEvent.KEYCODE_DPAD_CENTER}</li>
+     *     <li>{@link KeyEvent.KEYCODE_BACK}</li>
+     * </ul>
      *
      * @param event the event to send
      */
diff --git a/core/java/android/window/BackEvent.aidl b/core/java/android/hardware/input/VirtualDpadConfig.aidl
similarity index 90%
copy from core/java/android/window/BackEvent.aidl
copy to core/java/android/hardware/input/VirtualDpadConfig.aidl
index 821f1fa..fac90f4 100644
--- a/core/java/android/window/BackEvent.aidl
+++ b/core/java/android/hardware/input/VirtualDpadConfig.aidl
@@ -14,9 +14,6 @@
  * limitations under the License.
  */
 
-package android.window;
+package android.hardware.input;
 
-/**
- * @hide
- */
-parcelable BackEvent;
+parcelable VirtualDpadConfig;
diff --git a/core/java/android/hardware/input/VirtualDpadConfig.java b/core/java/android/hardware/input/VirtualDpadConfig.java
new file mode 100644
index 0000000..d888dc0
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualDpadConfig.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 android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Configurations to create virtual Dpad.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualDpadConfig extends VirtualInputDeviceConfig implements Parcelable {
+    @NonNull
+    public static final Creator<VirtualDpadConfig> CREATOR = new Creator<VirtualDpadConfig>() {
+        @Override
+        public VirtualDpadConfig createFromParcel(Parcel in) {
+            return new VirtualDpadConfig(in);
+        }
+
+        @Override
+        public VirtualDpadConfig[] newArray(int size) {
+            return new VirtualDpadConfig[size];
+        }
+    };
+
+    private VirtualDpadConfig(@NonNull VirtualDpadConfig.Builder builder) {
+        super(builder);
+    }
+
+    private VirtualDpadConfig(@NonNull Parcel in) {
+        super(in);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+    }
+
+    /**
+     * Builder for creating a {@link VirtualDpadConfig}.
+     */
+    public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
+
+        /**
+         * Builds the {@link VirtualDpadConfig} instance.
+         */
+        @NonNull
+        public VirtualDpadConfig build() {
+            return new VirtualDpadConfig(this);
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
new file mode 100644
index 0000000..d3dacc9
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+
+/**
+ * Common configurations to create virtual input devices.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class VirtualInputDeviceConfig {
+    /** The vendor id uniquely identifies the company who manufactured the device. */
+    private final int mVendorId;
+    /**
+     * The product id uniquely identifies which product within the address space of a given vendor,
+     * identified by the device's vendor id.
+     */
+    private final int mProductId;
+    /** The associated display ID of the virtual input device. */
+    private final int mAssociatedDisplayId;
+    /** The name of the virtual input device. */
+    @NonNull
+    private final String mInputDeviceName;
+
+    protected VirtualInputDeviceConfig(@NonNull Builder<? extends Builder<?>> builder) {
+        mVendorId = builder.mVendorId;
+        mProductId = builder.mProductId;
+        mAssociatedDisplayId = builder.mAssociatedDisplayId;
+        mInputDeviceName = builder.mInputDeviceName;
+    }
+
+    protected VirtualInputDeviceConfig(@NonNull Parcel in) {
+        mVendorId = in.readInt();
+        mProductId = in.readInt();
+        mAssociatedDisplayId = in.readInt();
+        mInputDeviceName = in.readString8();
+    }
+
+    /**
+     * The vendor id uniquely identifies the company who manufactured the device.
+     */
+    public int getVendorId() {
+        return mVendorId;
+    }
+
+    /**
+     * The product id uniquely identifies which product within the address space of a given vendor,
+     * identified by the device's vendor id.
+     */
+    public int getProductId() {
+        return mProductId;
+    }
+
+    /**
+     * The associated display ID of the virtual input device.
+     */
+    public int getAssociatedDisplayId() {
+        return mAssociatedDisplayId;
+    }
+
+    /**
+     * The name of the virtual input device.
+     */
+    @NonNull
+    public String getInputDeviceName() {
+        return mInputDeviceName;
+    }
+
+    void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mVendorId);
+        dest.writeInt(mProductId);
+        dest.writeInt(mAssociatedDisplayId);
+        dest.writeString8(mInputDeviceName);
+    }
+
+    /**
+     * A builder for {@link VirtualInputDeviceConfig}
+     *
+     * @param <T> The subclass to be built.
+     */
+    @SuppressWarnings({"StaticFinalBuilder", "MissingBuildMethod"})
+    public abstract static class Builder<T extends Builder<T>> {
+
+        private int mVendorId;
+        private int mProductId;
+        private int mAssociatedDisplayId;
+        @NonNull
+        private String mInputDeviceName;
+
+        /** @see VirtualInputDeviceConfig#getVendorId(). */
+        @NonNull
+        public T setVendorId(int vendorId) {
+            mVendorId = vendorId;
+            return self();
+        }
+
+
+        /** @see VirtualInputDeviceConfig#getProductId(). */
+        @NonNull
+        public T setProductId(int productId) {
+            mProductId = productId;
+            return self();
+        }
+
+        /** @see VirtualInputDeviceConfig#getAssociatedDisplayId(). */
+        @NonNull
+        public T setAssociatedDisplayId(int displayId) {
+            mAssociatedDisplayId = displayId;
+            return self();
+        }
+
+        /** @see VirtualInputDeviceConfig#getInputDeviceName(). */
+        @NonNull
+        public T setInputDeviceName(@NonNull String deviceName) {
+            mInputDeviceName = deviceName;
+            return self();
+        }
+
+        /**
+         * Each subclass should return itself to allow the builder to chain properly
+         */
+        T self() {
+            return (T) this;
+        }
+    }
+}
diff --git a/core/java/android/window/BackEvent.aidl b/core/java/android/hardware/input/VirtualKeyboardConfig.aidl
similarity index 89%
copy from core/java/android/window/BackEvent.aidl
copy to core/java/android/hardware/input/VirtualKeyboardConfig.aidl
index 821f1fa..8772e23 100644
--- a/core/java/android/window/BackEvent.aidl
+++ b/core/java/android/hardware/input/VirtualKeyboardConfig.aidl
@@ -14,9 +14,6 @@
  * limitations under the License.
  */
 
-package android.window;
+package android.hardware.input;
 
-/**
- * @hide
- */
-parcelable BackEvent;
+parcelable VirtualKeyboardConfig;
diff --git a/core/java/android/hardware/input/VirtualKeyboardConfig.java b/core/java/android/hardware/input/VirtualKeyboardConfig.java
new file mode 100644
index 0000000..9463857
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualKeyboardConfig.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * Configurations to create virtual keyboard.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualKeyboardConfig extends VirtualInputDeviceConfig implements Parcelable {
+
+    @NonNull
+    public static final Creator<VirtualKeyboardConfig> CREATOR =
+            new Creator<VirtualKeyboardConfig>() {
+                @Override
+                public VirtualKeyboardConfig createFromParcel(Parcel in) {
+                    return new VirtualKeyboardConfig(in);
+                }
+
+                @Override
+                public VirtualKeyboardConfig[] newArray(int size) {
+                    return new VirtualKeyboardConfig[size];
+                }
+            };
+
+    private VirtualKeyboardConfig(@NonNull Builder builder) {
+        super(builder);
+    }
+
+    private VirtualKeyboardConfig(@NonNull Parcel in) {
+        super(in);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+    }
+
+    /**
+     * Builder for creating a {@link VirtualKeyboardConfig}.
+     */
+    public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
+        /**
+         * Builds the {@link VirtualKeyboardConfig} instance.
+         */
+        @NonNull
+        public VirtualKeyboardConfig build() {
+            return new VirtualKeyboardConfig(this);
+        }
+    }
+}
diff --git a/core/java/android/window/BackEvent.aidl b/core/java/android/hardware/input/VirtualMouseConfig.aidl
similarity index 90%
copy from core/java/android/window/BackEvent.aidl
copy to core/java/android/hardware/input/VirtualMouseConfig.aidl
index 821f1fa..a0d5fb5 100644
--- a/core/java/android/window/BackEvent.aidl
+++ b/core/java/android/hardware/input/VirtualMouseConfig.aidl
@@ -14,9 +14,6 @@
  * limitations under the License.
  */
 
-package android.window;
+package android.hardware.input;
 
-/**
- * @hide
- */
-parcelable BackEvent;
+parcelable VirtualMouseConfig;
diff --git a/core/java/android/hardware/input/VirtualMouseConfig.java b/core/java/android/hardware/input/VirtualMouseConfig.java
new file mode 100644
index 0000000..7ad5d04
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualMouseConfig.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 android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Configurations to create virtual mouse.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualMouseConfig extends VirtualInputDeviceConfig implements Parcelable {
+    @NonNull
+    public static final Creator<VirtualMouseConfig> CREATOR = new Creator<VirtualMouseConfig>() {
+        @Override
+        public VirtualMouseConfig createFromParcel(Parcel in) {
+            return new VirtualMouseConfig(in);
+        }
+
+        @Override
+        public VirtualMouseConfig[] newArray(int size) {
+            return new VirtualMouseConfig[size];
+        }
+    };
+
+    private VirtualMouseConfig(@NonNull VirtualMouseConfig.Builder builder) {
+        super(builder);
+    }
+
+    private VirtualMouseConfig(@NonNull Parcel in) {
+        super(in);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+    }
+
+    /**
+     * Builder for creating a {@link VirtualMouseConfig}.
+     */
+    public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
+
+        /**
+         * Builds the {@link VirtualMouseConfig} instance.
+         */
+        @NonNull
+        public VirtualMouseConfig build() {
+            return new VirtualMouseConfig(this);
+        }
+    }
+}
diff --git a/core/java/android/window/BackEvent.aidl b/core/java/android/hardware/input/VirtualTouchscreenConfig.aidl
similarity index 89%
copy from core/java/android/window/BackEvent.aidl
copy to core/java/android/hardware/input/VirtualTouchscreenConfig.aidl
index 821f1fa..e4b0edb 100644
--- a/core/java/android/window/BackEvent.aidl
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.aidl
@@ -14,9 +14,6 @@
  * limitations under the License.
  */
 
-package android.window;
+package android.hardware.input;
 
-/**
- * @hide
- */
-parcelable BackEvent;
+parcelable VirtualTouchscreenConfig;
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
new file mode 100644
index 0000000..e358619
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Configurations to create virtual touchscreen.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable {
+
+    /** The touchscreen width in pixels. */
+    private final int mWidthInPixels;
+    /** The touchscreen height in pixels. */
+    private final int mHeightInPixels;
+
+    private VirtualTouchscreenConfig(@NonNull Builder builder) {
+        super(builder);
+        mWidthInPixels = builder.mWidthInPixels;
+        mHeightInPixels = builder.mHeightInPixels;
+    }
+
+    private VirtualTouchscreenConfig(@NonNull Parcel in) {
+        super(in);
+        mWidthInPixels = in.readInt();
+        mHeightInPixels = in.readInt();
+    }
+
+    /** Returns the touchscreen width in pixels. */
+    public int getWidthInPixels() {
+        return mWidthInPixels;
+    }
+
+    /** Returns the touchscreen height in pixels. */
+    public int getHeightInPixels() {
+        return mHeightInPixels;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeInt(mWidthInPixels);
+        dest.writeInt(mHeightInPixels);
+    }
+
+    @NonNull
+    public static final Creator<VirtualTouchscreenConfig> CREATOR =
+            new Creator<VirtualTouchscreenConfig>() {
+                @Override
+                public VirtualTouchscreenConfig createFromParcel(Parcel in) {
+                    return new VirtualTouchscreenConfig(in);
+                }
+
+                @Override
+                public VirtualTouchscreenConfig[] newArray(int size) {
+                    return new VirtualTouchscreenConfig[size];
+                }
+            };
+
+    /**
+     * Builder for creating a {@link VirtualTouchscreenConfig}.
+     */
+    public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
+        private int mWidthInPixels;
+        private int mHeightInPixels;
+
+        /**
+         * @see VirtualTouchscreenConfig#getWidthInPixels().
+         */
+        @NonNull
+        public Builder setWidthInPixels(int widthInPixels) {
+            mWidthInPixels = widthInPixels;
+            return this;
+        }
+
+        /**
+         * @see VirtualTouchscreenConfig#getHeightInPixels().
+         */
+        @NonNull
+        public Builder setHeightInPixels(int heightInPixels) {
+            mHeightInPixels = heightInPixels;
+            return this;
+        }
+
+        /**
+         * Builds the {@link VirtualTouchscreenConfig} instance.
+         */
+        @NonNull
+        public VirtualTouchscreenConfig build() {
+            return new VirtualTouchscreenConfig(this);
+        }
+    }
+}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 8278e89..def0cbd 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -355,6 +355,26 @@
     }
 
     /**
+     * Return the Linux UID assigned to the process that sent the transaction
+     * currently being processed.
+     *
+     * Logs WTF if the current thread is not currently
+     * executing an incoming transaction and the calling identity has not been
+     * explicitly set with {@link #clearCallingIdentity()}
+     *
+     * @hide
+     */
+    public static final int getCallingUidOrWtf() {
+        if (!isDirectlyHandlingTransaction() && !hasExplicitIdentity()) {
+            Log.wtfStack(TAG,
+                    "Thread is not in a binder transaction, "
+                            + "and the calling identity has not been "
+                            + "explicitly set with clearCallingIdentity");
+        }
+        return getCallingUid();
+    }
+
+    /**
      * Return the UserHandle assigned to the process that sent you the
      * current transaction that is being processed. This is the user
      * of the caller. It is distinct from {@link #getCallingUid()} in that a
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 44a1fa5..249f486 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -1446,28 +1446,6 @@
         return IS_DEBUGGABLE;
     }
 
-
-    /**
-     * Returns true if the device is running a secure build, such as "user" or "userdebug".
-     *
-     * Secure builds drop adbd privileges by default, though debuggable builds still allow users
-     * to gain root access via local shell. See should_drop_privileges() in adb for details.
-     * @hide
-     */
-    private static final boolean IS_SECURE =
-            SystemProperties.getBoolean("ro.secure", true);
-    /**
-     * Returns true if the device is running a secure build, such as "user" or "userdebug".
-     *
-     * Secure builds drop adbd privileges by default, though debuggable builds still allow users
-     * to gain root access via local shell. See should_drop_privileges() in adb for details.
-     * @hide
-     */
-    @TestApi
-    public static boolean isSecure() {
-        return IS_SECURE;
-    }
-
     /** {@hide} */
     public static final boolean IS_ENG = "eng".equals(TYPE);
     /** {@hide} */
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index dd02e02..07c4b44 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -59,7 +59,7 @@
 import android.graphics.drawable.Drawable;
 import android.location.LocationManager;
 import android.provider.Settings;
-import android.telephony.TelephonyManager;
+import android.telecom.TelecomManager;
 import android.util.AndroidException;
 import android.util.ArraySet;
 import android.util.Log;
@@ -2139,13 +2139,8 @@
                 mContext.getContentResolver(),
                 Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
         boolean isSystemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
-        boolean inCall = false;
-        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
-        if (telephonyManager != null) {
-            inCall = telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE;
-        }
         boolean isUserSwitchDisallowed = hasUserRestrictionForUser(DISALLOW_USER_SWITCH, mUserId);
-        return (allowUserSwitchingWhenSystemUserLocked || isSystemUserUnlocked) && !inCall
+        return (allowUserSwitchingWhenSystemUserLocked || isSystemUserUnlocked) && !inCall()
                 && !isUserSwitchDisallowed;
     }
 
@@ -2184,11 +2179,8 @@
             android.Manifest.permission.MANAGE_USERS,
             android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     public @UserSwitchabilityResult int getUserSwitchability(UserHandle userHandle) {
-        final TelephonyManager tm =
-                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
-
         int flags = SWITCHABILITY_STATUS_OK;
-        if (tm.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
+        if (inCall()) {
             flags |= SWITCHABILITY_STATUS_USER_IN_CALL;
         }
         if (hasUserRestrictionForUser(DISALLOW_USER_SWITCH, userHandle)) {
@@ -5622,6 +5614,11 @@
         }
     }
 
+    private boolean inCall() {
+        final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+        return telecomManager != null && telecomManager.isInCall();
+    }
+
     /* Cache key for anything that assumes that userIds cannot be re-used without rebooting. */
     private static final String CACHE_KEY_STATIC_USER_PROPERTIES = "cache_key.static_user_props";
 
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index ca88ae3..7df9290 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -23,9 +23,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
-import android.annotation.TestApi;
-import android.content.pm.PackageManager;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.provider.Settings.Config.SyncDisabledMode;
@@ -55,13 +54,6 @@
 @SystemApi
 public final class DeviceConfig {
     /**
-     * The content:// style URL for the config table.
-     *
-     * @hide
-     */
-    public static final Uri CONTENT_URI = Uri.parse("content://" + Settings.AUTHORITY + "/config");
-
-    /**
      * Namespace for activity manager related features. These features will be applied
      * immediately upon change.
      *
@@ -75,6 +67,7 @@
      * different namespace in order to avoid phonetype from resetting it.
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_ACTIVITY_MANAGER_COMPONENT_ALIAS = "activity_manager_ca";
 
     /**
@@ -93,7 +86,6 @@
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
-    @TestApi
     public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager";
 
     /**
@@ -231,7 +223,6 @@
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
-    @TestApi
     public static final String NAMESPACE_DEVICE_IDLE = "device_idle";
 
     /**
@@ -291,6 +282,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_INTELLIGENCE_CONTENT_SUGGESTIONS =
             "intelligence_content_suggestions";
 
@@ -298,7 +290,7 @@
      * Namespace for JobScheduler configurations.
      * @hide
      */
-    @TestApi
+    @SystemApi
     public static final String NAMESPACE_JOB_SCHEDULER = "jobscheduler";
 
     /**
@@ -338,6 +330,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_MGLRU_NATIVE = "mglru_native";
 
     /**
@@ -393,6 +386,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_REMOTE_KEY_PROVISIONING_NATIVE =
             "remote_key_provisioning_native";
 
@@ -417,6 +411,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_ROTATION_RESOLVER = "rotation_resolver";
 
     /**
@@ -468,6 +463,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_SETTINGS_STATS = "settings_stats";
 
     /**
@@ -567,6 +563,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_TARE = "tare";
 
     /**
@@ -591,6 +588,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_CONTACTS_PROVIDER = "contacts_provider";
 
     /**
@@ -598,6 +596,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_SETTINGS_UI = "settings_ui";
 
     /**
@@ -608,7 +607,7 @@
      *
      * @hide
      */
-    @TestApi
+    @SystemApi
     public static final String NAMESPACE_ANDROID = "android";
 
     /**
@@ -616,6 +615,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_WINDOW_MANAGER = "window_manager";
 
     /**
@@ -632,7 +632,7 @@
      *
      * @hide
      */
-    @TestApi
+    @SystemApi
     public static final String NAMESPACE_SELECTION_TOOLBAR = "selection_toolbar";
 
     /**
@@ -640,6 +640,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_VOICE_INTERACTION = "voice_interaction";
 
     /**
@@ -647,6 +648,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_DEVICE_POLICY_MANAGER =
             "device_policy_manager";
 
@@ -697,6 +699,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_WIDGET = "widget";
 
     /**
@@ -704,6 +707,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_CONNECTIVITY_THERMAL_POWER_MANAGER =
             "connectivity_thermal_power_manager";
 
@@ -712,6 +716,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_CONFIGURATION = "configuration";
 
     /**
@@ -719,6 +724,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_LATENCY_TRACKER = "latency_tracker";
 
     /**
@@ -726,6 +732,8 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @SuppressLint("IntentName")
     public static final String NAMESPACE_INTERACTION_JANK_MONITOR = "interaction_jank_monitor";
 
     /**
@@ -733,6 +741,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_GAME_OVERLAY = "game_overlay";
 
     /**
@@ -740,6 +749,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE =
             "virtualization_framework_native";
 
@@ -748,7 +758,7 @@
      *
      * @hide
      */
-    @TestApi
+    @SystemApi
     public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
 
     /**
@@ -756,7 +766,7 @@
      *
      * @hide
      */
-    @TestApi
+    @SystemApi
     public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
 
     /**
@@ -790,6 +800,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE = "vendor_system_native";
 
     /**
@@ -797,6 +808,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT = "vendor_system_native_boot";
 
     /**
@@ -804,6 +816,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_MEMORY_SAFETY_NATIVE = "memory_safety_native";
 
     /**
@@ -811,6 +824,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_WEAR = "wear";
 
     /**
@@ -826,7 +840,7 @@
      *
      * @hide
      */
-    @TestApi
+    @SystemApi
     public static final String NAMESPACE_INPUT_METHOD_MANAGER = "input_method_manager";
 
     /**
@@ -842,6 +856,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final String NAMESPACE_ARC_APP_COMPAT = "arc_app_compat";
 
     private static final Object sLock = new Object();
@@ -1119,6 +1134,7 @@
      * @see #getSyncDisabledMode()
      * @hide
      */
+    @SystemApi
     @RequiresPermission(WRITE_DEVICE_CONFIG)
     public static void setSyncDisabledMode(@SyncDisabledMode int syncDisabledMode) {
         Settings.Config.setSyncDisabledMode(syncDisabledMode);
@@ -1130,6 +1146,7 @@
      * @see #setSyncDisabledMode(int)
      * @hide
      */
+    @SystemApi
     @RequiresPermission(WRITE_DEVICE_CONFIG)
     public static @SyncDisabledMode int getSyncDisabledMode() {
         return Settings.Config.getSyncDisabledMode();
@@ -1150,12 +1167,10 @@
      * @see #removeOnPropertiesChangedListener(OnPropertiesChangedListener)
      */
     @SystemApi
-    @RequiresPermission(READ_DEVICE_CONFIG)
     public static void addOnPropertiesChangedListener(
             @NonNull String namespace,
             @NonNull @CallbackExecutor Executor executor,
             @NonNull OnPropertiesChangedListener onPropertiesChangedListener) {
-        enforceReadPermission(namespace);
         synchronized (sLock) {
             Pair<String, Executor> oldNamespace = sListeners.get(onPropertiesChangedListener);
             if (oldNamespace == null) {
@@ -1194,11 +1209,6 @@
         }
     }
 
-    private static Uri createNamespaceUri(@NonNull String namespace) {
-        Preconditions.checkNotNull(namespace);
-        return CONTENT_URI.buildUpon().appendPath(namespace).build();
-    }
-
     /**
      * Increment the count used to represent the number of listeners subscribed to the given
      * namespace. If this is the first (i.e. incrementing from 0 to 1) for the given namespace, a
@@ -1223,7 +1233,7 @@
                 }
             };
             Settings.Config
-                    .registerContentObserver(createNamespaceUri(namespace), true, contentObserver);
+                    .registerContentObserver(namespace, true, contentObserver);
             sNamespaces.put(namespace, new Pair<>(contentObserver, 1));
         }
     }
@@ -1283,23 +1293,10 @@
     }
 
     /**
-     * Enforces READ_DEVICE_CONFIG permission if namespace is not one of public namespaces.
-     * @hide
-     */
-    public static void enforceReadPermission(String namespace) {
-        if (Settings.Config.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
-                != PackageManager.PERMISSION_GRANTED) {
-            if (!PUBLIC_NAMESPACES.contains(namespace)) {
-                throw new SecurityException("Permission denial: reading from settings requires:"
-                        + READ_DEVICE_CONFIG);
-            }
-        }
-    }
-
-    /**
      * Returns list of namespaces that can be read without READ_DEVICE_CONFIG_PERMISSION;
      * @hide
      */
+    @SystemApi
     public static @NonNull List<String> getPublicNamespaces() {
         return PUBLIC_NAMESPACES;
     }
@@ -1356,6 +1353,7 @@
          * @param keyValueMap A map between property names and property values.
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         public Properties(@NonNull String namespace, @Nullable Map<String, String> keyValueMap) {
             Preconditions.checkNotNull(namespace);
             mNamespace = namespace;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 22d5a27..03846db 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -599,8 +599,8 @@
      * the result is set to {@link android.app.Activity#RESULT_CANCELED}.
      */
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
-    public static final String ACTION_MANAGE_APP_LONG_JOBS =
-            "android.settings.MANAGE_APP_LONG_JOBS";
+    public static final String ACTION_MANAGE_APP_LONG_RUNNING_JOBS =
+            "android.settings.MANAGE_APP_LONG_RUNNING_JOBS";
 
     /**
      * Activity Action: Show settings to allow configuration of cross-profile access for apps
@@ -2819,6 +2819,7 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     @TestApi
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int RESET_MODE_PACKAGE_DEFAULTS = 1;
 
     /**
@@ -2828,6 +2829,7 @@
      * the setting will be deleted. This mode is only available to the system.
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int RESET_MODE_UNTRUSTED_DEFAULTS = 2;
 
     /**
@@ -2838,6 +2840,7 @@
      * This mode is only available to the system.
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int RESET_MODE_UNTRUSTED_CHANGES = 3;
 
     /**
@@ -2849,6 +2852,7 @@
      * to the system.
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int RESET_MODE_TRUSTED_DEFAULTS = 4;
 
     /** @hide */
@@ -3362,7 +3366,7 @@
         public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix,
                 List<String> names) {
             String namespace = prefix.substring(0, prefix.length() - 1);
-            DeviceConfig.enforceReadPermission(namespace);
+            Config.enforceReadPermission(namespace);
             ArrayMap<String, String> keyValues = new ArrayMap<>();
             int currentGeneration = -1;
 
@@ -7224,6 +7228,15 @@
         public static final String SHOW_IME_WITH_HARD_KEYBOARD = "show_ime_with_hard_keyboard";
 
         /**
+         * Whether stylus button presses are disabled. This is a boolean that
+         * determines if stylus buttons are ignored.
+         *
+         * @hide
+         */
+        @SuppressLint("NoSettingsProvider")
+        public static final String STYLUS_BUTTONS_DISABLED = "stylus_buttons_disabled";
+
+        /**
          * Host name and port for global http proxy. Uses ':' seperator for
          * between host and port.
          *
@@ -17992,6 +18005,7 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final class Config extends NameValueTable {
 
         /**
@@ -18026,12 +18040,19 @@
          */
         public static final int SYNC_DISABLED_MODE_UNTIL_REBOOT = 2;
 
+        /**
+         * The content:// style URL for the config table.
+         *
+         * @hide
+         */
+        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/config");
+
         private static final ContentProviderHolder sProviderHolder =
-                new ContentProviderHolder(DeviceConfig.CONTENT_URI);
+                new ContentProviderHolder(CONTENT_URI);
 
         // Populated lazily, guarded by class object:
         private static final NameValueCache sNameValueCache = new NameValueCache(
-                DeviceConfig.CONTENT_URI,
+                CONTENT_URI,
                 CALL_METHOD_GET_CONFIG,
                 CALL_METHOD_PUT_CONFIG,
                 CALL_METHOD_DELETE_CONFIG,
@@ -18040,6 +18061,10 @@
                 sProviderHolder,
                 Config.class);
 
+        // Should never be invoked
+        private Config() {
+        }
+
         /**
          * Look up a name in the database.
          * @param name to look up in the table
@@ -18047,8 +18072,10 @@
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        @Nullable
         @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
-        static String getString(String name) {
+        public static String getString(@NonNull String name) {
             ContentResolver resolver = getContentResolver();
             return sNameValueCache.getStringForUser(resolver, name, resolver.getUserId());
         }
@@ -18063,6 +18090,8 @@
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        @NonNull
         @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
         public static Map<String, String> getStrings(@NonNull String namespace,
                 @NonNull List<String> names) {
@@ -18119,6 +18148,7 @@
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
         public static boolean putString(@NonNull String namespace,
                 @NonNull String name, @Nullable String value, boolean makeDefault) {
@@ -18138,6 +18168,7 @@
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
         public static boolean setStrings(@NonNull String namespace,
                 @NonNull Map<String, String> keyValues)
@@ -18188,8 +18219,9 @@
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static boolean deleteString(@NonNull String namespace,
+        public static boolean deleteString(@NonNull String namespace,
                 @NonNull String name) {
             ContentResolver resolver = getContentResolver();
             return sNameValueCache.deleteStringForUser(resolver,
@@ -18209,8 +18241,9 @@
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static void resetToDefaults(@ResetMode int resetMode,
+        public static void resetToDefaults(@ResetMode int resetMode,
                 @Nullable String namespace) {
             try {
                 ContentResolver resolver = getContentResolver();
@@ -18224,7 +18257,7 @@
                 cp.call(resolver.getAttributionSource(),
                         sProviderHolder.mUri.getAuthority(), CALL_METHOD_RESET_CONFIG, null, arg);
             } catch (RemoteException e) {
-                Log.w(TAG, "Can't reset to defaults for " + DeviceConfig.CONTENT_URI, e);
+                Log.w(TAG, "Can't reset to defaults for " + CONTENT_URI, e);
             }
         }
 
@@ -18234,9 +18267,10 @@
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @SuppressLint("AndroidFrameworkRequiresPermission")
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static void setSyncDisabledMode(@SyncDisabledMode int disableSyncMode) {
+        public static void setSyncDisabledMode(@SyncDisabledMode int disableSyncMode) {
             try {
                 ContentResolver resolver = getContentResolver();
                 Bundle args = new Bundle();
@@ -18245,7 +18279,7 @@
                 cp.call(resolver.getAttributionSource(), sProviderHolder.mUri.getAuthority(),
                         CALL_METHOD_SET_SYNC_DISABLED_MODE_CONFIG, null, args);
             } catch (RemoteException e) {
-                Log.w(TAG, "Can't set sync disabled mode " + DeviceConfig.CONTENT_URI, e);
+                Log.w(TAG, "Can't set sync disabled mode " + CONTENT_URI, e);
             }
         }
 
@@ -18255,9 +18289,10 @@
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         @SuppressLint("AndroidFrameworkRequiresPermission")
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static int getSyncDisabledMode() {
+        public static int getSyncDisabledMode() {
             try {
                 ContentResolver resolver = getContentResolver();
                 Bundle args = Bundle.EMPTY;
@@ -18268,7 +18303,7 @@
                         null, args);
                 return bundle.getInt(KEY_CONFIG_GET_SYNC_DISABLED_MODE_RETURN);
             } catch (RemoteException e) {
-                Log.w(TAG, "Can't query sync disabled mode " + DeviceConfig.CONTENT_URI, e);
+                Log.w(TAG, "Can't query sync disabled mode " + CONTENT_URI, e);
             }
             return -1;
         }
@@ -18288,21 +18323,26 @@
 
 
         /**
-         * Register a content observer
+         * Register a content observer.
          *
          * @hide
          */
-        public static void registerContentObserver(@NonNull Uri uri, boolean notifyForDescendants,
-                @NonNull ContentObserver observer) {
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        public static void registerContentObserver(@Nullable String namespace,
+                boolean notifyForDescendants, @NonNull ContentObserver observer) {
             ActivityThread.currentApplication().getContentResolver()
-               .registerContentObserver(uri, notifyForDescendants, observer);
+                    .registerContentObserver(createNamespaceUri(namespace),
+                         notifyForDescendants, observer);
         }
 
         /**
-         * Unregister a content observer
+         * Unregister a content observer.
+         * this may only be used with content observers registered through
+         * {@link Config#registerContentObserver}
          *
          * @hide
          */
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
         public static void unregisterContentObserver(@NonNull ContentObserver observer) {
             ActivityThread.currentApplication().getContentResolver()
               .unregisterContentObserver(observer);
@@ -18330,6 +18370,21 @@
                .getApplicationContext().checkCallingOrSelfPermission(permission);
         }
 
+        /**
+         * Enforces READ_DEVICE_CONFIG permission if namespace is not one of public namespaces.
+         * @hide
+         */
+        public static void enforceReadPermission(String namespace) {
+            if (ActivityThread.currentApplication().getApplicationContext()
+                    .checkCallingOrSelfPermission(Manifest.permission.READ_DEVICE_CONFIG)
+                    != PackageManager.PERMISSION_GRANTED) {
+                if (!DeviceConfig.getPublicNamespaces().contains(namespace)) {
+                    throw new SecurityException("Permission denial: reading from settings requires:"
+                        + Manifest.permission.READ_DEVICE_CONFIG);
+                }
+            }
+        }
+
         private static void registerMonitorCallbackAsUser(
                 @NonNull ContentResolver resolver, @UserIdInt int userHandle,
                 @NonNull RemoteCallback callback) {
@@ -18363,6 +18418,11 @@
             return namespace + "/";
         }
 
+        private static Uri createNamespaceUri(@NonNull String namespace) {
+            Preconditions.checkNotNull(namespace);
+            return CONTENT_URI.buildUpon().appendPath(namespace).build();
+        }
+
         private static ContentResolver getContentResolver() {
             return ActivityThread.currentApplication().getContentResolver();
         }
diff --git a/core/java/android/security/keymaster/KeymasterDefs.java b/core/java/android/security/keymaster/KeymasterDefs.java
index e720f1a..4d6422c 100644
--- a/core/java/android/security/keymaster/KeymasterDefs.java
+++ b/core/java/android/security/keymaster/KeymasterDefs.java
@@ -126,6 +126,8 @@
             Tag.BOOT_PATCHLEVEL; // KM_UINT | 719;
     public static final int KM_TAG_DEVICE_UNIQUE_ATTESTATION =
             Tag.DEVICE_UNIQUE_ATTESTATION; // KM_BOOL | 720;
+    public static final int KM_TAG_ATTESTATION_ID_SECOND_IMEI =
+            Tag.ATTESTATION_ID_SECOND_IMEI; // KM_BYTES | 723;
 
     public static final int KM_TAG_NONCE = Tag.NONCE; // KM_BYTES | 1001;
     public static final int KM_TAG_MAC_LENGTH = Tag.MAC_LENGTH; // KM_UINT | 1003;
diff --git a/core/java/android/service/autofill/augmented/AugmentedAutofillService.java b/core/java/android/service/autofill/augmented/AugmentedAutofillService.java
index d9a310f..745f36d 100644
--- a/core/java/android/service/autofill/augmented/AugmentedAutofillService.java
+++ b/core/java/android/service/autofill/augmented/AugmentedAutofillService.java
@@ -332,7 +332,6 @@
     }
 
     @Override
-    /** @hide */
     protected final void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.print("Service component: "); pw.println(
                 ComponentName.flattenToShortString(mServiceComponentName));
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index d2a4ae2..9396a88 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -69,6 +69,18 @@
             "android.service.controls.META_DATA_PANEL_ACTIVITY";
 
     /**
+     * Boolean extra containing the value of
+     * {@link android.provider.Settings.Secure#LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS}.
+     *
+     * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY}
+     * is launched.
+     *
+     * @hide
+     */
+    public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS =
+            "android.service.controls.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
+
+    /**
      * @hide
      */
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java
index 05c86172..cc83dec 100644
--- a/core/java/android/util/SparseArray.java
+++ b/core/java/android/util/SparseArray.java
@@ -525,9 +525,10 @@
             return false;
         }
 
+        // size() calls above took care about gc() compaction.
         for (int index = 0; index < size; index++) {
-            int key = keyAt(index);
-            if (!Objects.equals(valueAt(index), other.get(key))) {
+            if (mKeys[index] != other.mKeys[index]
+                    || !Objects.equals(mValues[index], other.mValues[index])) {
                 return false;
             }
         }
@@ -545,10 +546,11 @@
     public int contentHashCode() {
         int hash = 0;
         int size = size();
+        // size() call above took care about gc() compaction.
         for (int index = 0; index < size; index++) {
-            int key = keyAt(index);
-            E value = valueAt(index);
-            hash = 31 * hash + Objects.hashCode(key);
+            int key = mKeys[index];
+            E value = (E) mValues[index];
+            hash = 31 * hash + key;
             hash = 31 * hash + Objects.hashCode(value);
         }
         return hash;
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index 169c8c5..ce7606a0 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -227,8 +227,10 @@
      * timebase.
      * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
      * @param modeId The new mode Id
+     * @param renderPeriod The render frame period, which is a multiple of the mode's vsync period
      */
-    public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
+    public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+            long renderPeriod) {
     }
 
     /**
@@ -303,8 +305,9 @@
 
     // Called from native code.
     @SuppressWarnings("unused")
-    private void dispatchModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
-        onModeChanged(timestampNanos, physicalDisplayId, modeId);
+    private void dispatchModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+            long renderPeriod) {
+        onModeChanged(timestampNanos, physicalDisplayId, modeId, renderPeriod);
     }
 
     // Called from native code.
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 138017c..85cb517 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -180,6 +180,16 @@
     public int modeId;
 
     /**
+     * The render frame rate this display is scheduled at, which is a divisor of the active mode
+     * refresh rate. This is the rate SurfaceFlinger would consume frames and would be observable
+     * by applications via the cadence of {@link android.view.Choreographer} callbacks and
+     * by backpressure when submitting buffers as fast as possible.
+     * Apps can call {@link android.view.Display#getRefreshRate} to query this value.
+     *
+     */
+    public float renderFrameRate;
+
+    /**
      * The default display mode.
      */
     public int defaultModeId;
@@ -376,6 +386,7 @@
                 && Objects.equals(displayCutout, other.displayCutout)
                 && rotation == other.rotation
                 && modeId == other.modeId
+                && renderFrameRate == other.renderFrameRate
                 && defaultModeId == other.defaultModeId
                 && Arrays.equals(supportedModes, other.supportedModes)
                 && colorMode == other.colorMode
@@ -428,6 +439,7 @@
         displayCutout = other.displayCutout;
         rotation = other.rotation;
         modeId = other.modeId;
+        renderFrameRate = other.renderFrameRate;
         defaultModeId = other.defaultModeId;
         supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length);
         colorMode = other.colorMode;
@@ -475,6 +487,7 @@
         displayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(source);
         rotation = source.readInt();
         modeId = source.readInt();
+        renderFrameRate = source.readFloat();
         defaultModeId = source.readInt();
         int nModes = source.readInt();
         supportedModes = new Display.Mode[nModes];
@@ -535,6 +548,7 @@
         DisplayCutout.ParcelableWrapper.writeCutoutToParcel(displayCutout, dest, flags);
         dest.writeInt(rotation);
         dest.writeInt(modeId);
+        dest.writeFloat(renderFrameRate);
         dest.writeInt(defaultModeId);
         dest.writeInt(supportedModes.length);
         for (int i = 0; i < supportedModes.length; i++) {
@@ -764,6 +778,7 @@
         sb.append(presentationDeadlineNanos);
         sb.append(", mode ");
         sb.append(modeId);
+        sb.append(renderFrameRate);
         sb.append(", defaultMode ");
         sb.append(defaultModeId);
         sb.append(", modes ");
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 0743ccb..6d9f99f 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -53,7 +53,6 @@
 import android.view.KeyEvent;
 import android.view.InputEvent;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.MagnificationSpec;
 import android.view.MotionEvent;
 import android.view.InputChannel;
diff --git a/core/java/android/view/InsetsVisibilities.aidl b/core/java/android/view/InsetsVisibilities.aidl
deleted file mode 100644
index bd573ea..0000000
--- a/core/java/android/view/InsetsVisibilities.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2021, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.view;
-
-parcelable InsetsVisibilities;
diff --git a/core/java/android/view/InsetsVisibilities.java b/core/java/android/view/InsetsVisibilities.java
deleted file mode 100644
index 7d259fb..0000000
--- a/core/java/android/view/InsetsVisibilities.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.view;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import java.util.Arrays;
-import java.util.StringJoiner;
-
-/**
- * A collection of visibilities of insets. This is used for carrying the requested visibilities.
- * @hide
- */
-public class InsetsVisibilities implements Parcelable {
-
-    private static final int UNSPECIFIED = 0;
-    private static final int VISIBLE = 1;
-    private static final int INVISIBLE = -1;
-
-    private final int[] mVisibilities = new int[InsetsState.SIZE];
-
-    public InsetsVisibilities() {
-    }
-
-    public InsetsVisibilities(InsetsVisibilities other) {
-        set(other);
-    }
-
-    public InsetsVisibilities(Parcel in) {
-        in.readIntArray(mVisibilities);
-    }
-
-    /**
-     * Copies from another {@link InsetsVisibilities}.
-     *
-     * @param other an instance of {@link InsetsVisibilities}.
-     */
-    public void set(InsetsVisibilities other) {
-        System.arraycopy(other.mVisibilities, InsetsState.FIRST_TYPE, mVisibilities,
-                InsetsState.FIRST_TYPE, InsetsState.SIZE);
-    }
-
-    /**
-     * Sets a visibility to a type.
-     *
-     * @param type The {@link @InsetsState.InternalInsetsType}.
-     * @param visible {@code true} represents visible; {@code false} represents invisible.
-     */
-    public void setVisibility(@InsetsState.InternalInsetsType int type, boolean visible) {
-        mVisibilities[type] = visible ? VISIBLE : INVISIBLE;
-    }
-
-    /**
-     * Returns the specified insets visibility of the type. If it has never been specified,
-     * this returns the default visibility.
-     *
-     * @param type The {@link @InsetsState.InternalInsetsType}.
-     * @return The specified visibility or the default one if it is not specified.
-     */
-    public boolean getVisibility(@InsetsState.InternalInsetsType int type) {
-        final int visibility = mVisibilities[type];
-        return visibility == UNSPECIFIED
-                ? InsetsState.getDefaultVisibility(type)
-                : visibility == VISIBLE;
-    }
-
-    @Override
-    public String toString() {
-        StringJoiner joiner = new StringJoiner(", ");
-        for (int type = InsetsState.FIRST_TYPE; type <= InsetsState.LAST_TYPE; type++) {
-            final int visibility = mVisibilities[type];
-            if (visibility != UNSPECIFIED) {
-                joiner.add(InsetsState.typeToString(type) + ": "
-                        + (visibility == VISIBLE ? "visible" : "invisible"));
-            }
-        }
-        return joiner.toString();
-    }
-
-    @Override
-    public int hashCode() {
-        return Arrays.hashCode(mVisibilities);
-    }
-
-    @Override
-    public boolean equals(Object other) {
-        if (!(other instanceof InsetsVisibilities)) {
-            return false;
-        }
-        return Arrays.equals(mVisibilities, ((InsetsVisibilities) other).mVisibilities);
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeIntArray(mVisibilities);
-    }
-
-    public void readFromParcel(@NonNull Parcel in) {
-        in.readIntArray(mVisibilities);
-    }
-
-    public static final @NonNull Creator<InsetsVisibilities> CREATOR =
-            new Creator<InsetsVisibilities>() {
-
-        public InsetsVisibilities createFromParcel(Parcel in) {
-            return new InsetsVisibilities(in);
-        }
-
-        public InsetsVisibilities[] newArray(int size) {
-            return new InsetsVisibilities[size];
-        }
-    };
-}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 30e7a7a..89a0c05 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -1504,6 +1504,7 @@
     public static final class DynamicDisplayInfo {
         public DisplayMode[] supportedDisplayModes;
         public int activeDisplayModeId;
+        public float renderFrameRate;
 
         public int[] supportedColorModes;
         public int activeColorMode;
@@ -1520,6 +1521,7 @@
             return "DynamicDisplayInfo{"
                     + "supportedDisplayModes=" + Arrays.toString(supportedDisplayModes)
                     + ", activeDisplayModeId=" + activeDisplayModeId
+                    + ", renderFrameRate=" + renderFrameRate
                     + ", supportedColorModes=" + Arrays.toString(supportedColorModes)
                     + ", activeColorMode=" + activeColorMode
                     + ", hdrCapabilities=" + hdrCapabilities
@@ -1535,6 +1537,7 @@
             DynamicDisplayInfo that = (DynamicDisplayInfo) o;
             return Arrays.equals(supportedDisplayModes, that.supportedDisplayModes)
                 && activeDisplayModeId == that.activeDisplayModeId
+                && renderFrameRate == that.renderFrameRate
                 && Arrays.equals(supportedColorModes, that.supportedColorModes)
                 && activeColorMode == that.activeColorMode
                 && Objects.equals(hdrCapabilities, that.hdrCapabilities)
@@ -1544,7 +1547,7 @@
         @Override
         public int hashCode() {
             return Objects.hash(Arrays.hashCode(supportedDisplayModes), activeDisplayModeId,
-                    activeColorMode, hdrCapabilities);
+                    renderFrameRate, activeColorMode, hdrCapabilities);
         }
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 09a9d46..bfb489e 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -77,6 +77,7 @@
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
@@ -505,6 +506,13 @@
     Region mTouchableRegion;
     Region mPreviousTouchableRegion;
 
+    private int mMeasuredWidth;
+    private int mMeasuredHeight;
+
+    // This indicates that we've already known the window size but without measuring the views.
+    // If this is true, we must measure the views before laying out them.
+    private boolean mViewMeasureDeferred;
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     int mWidth;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -2593,7 +2601,8 @@
     }
 
     private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
-            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
+            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight,
+            boolean forRootSizeOnly) {
         int childWidthMeasureSpec;
         int childHeightMeasureSpec;
         boolean windowSizeMayChange = false;
@@ -2649,7 +2658,15 @@
                     lp.privateFlags);
             childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
                     lp.privateFlags);
-            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+            if (!forRootSizeOnly || !setMeasuredRootSizeFromSpec(
+                    childWidthMeasureSpec, childHeightMeasureSpec)) {
+                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+            } else {
+                // We already know how big the window should be before measuring the views.
+                // We can measure the views before laying out them. This is to avoid unnecessary
+                // measure.
+                mViewMeasureDeferred = true;
+            }
             if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                 windowSizeMayChange = true;
             }
@@ -2665,6 +2682,25 @@
     }
 
     /**
+     * Sets the measured root size for requesting the window frame.
+     *
+     * @param widthMeasureSpec contains the size and the mode of the width.
+     * @param heightMeasureSpec contains the size and the mode of the height.
+     * @return {@code true} if we actually set the measured size; {@code false} otherwise.
+     */
+    private boolean setMeasuredRootSizeFromSpec(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
+            // We don't know the exact size. We need to measure the hierarchy to know that.
+            return false;
+        }
+        mMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+        mMeasuredHeight = MeasureSpec.getSize(heightMeasureSpec);
+        return true;
+    }
+
+    /**
      * Modifies the input matrix such that it maps view-local coordinates to
      * on-screen coordinates.
      *
@@ -2751,6 +2787,14 @@
                 || lp.type == TYPE_VOLUME_OVERLAY;
     }
 
+    /**
+     * @return {@code true} if we should reduce unnecessary measure for the window.
+     * TODO(b/260382739): Apply this to all windows.
+     */
+    private static boolean shouldOptimizeMeasure(final WindowManager.LayoutParams lp) {
+        return lp.type == TYPE_NOTIFICATION_SHADE;
+    }
+
     private Rect getWindowBoundsInsetSystemBars() {
         final Rect bounds = new Rect(
                 mContext.getResources().getConfiguration().windowConfiguration.getBounds());
@@ -2801,6 +2845,7 @@
         mAppVisibilityChanged = false;
         final boolean viewUserVisibilityChanged = !mFirst &&
                 ((mViewVisibility == View.VISIBLE) != (viewVisibility == View.VISIBLE));
+        final boolean shouldOptimizeMeasure = shouldOptimizeMeasure(lp);
 
         WindowManager.LayoutParams params = null;
         CompatibilityInfo compatibilityInfo =
@@ -2922,7 +2967,7 @@
 
             // Ask host how big it wants to be
             windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(),
-                    desiredWindowWidth, desiredWindowHeight);
+                    desiredWindowWidth, desiredWindowHeight, shouldOptimizeMeasure);
         }
 
         if (collectViewAttributes()) {
@@ -2962,8 +3007,8 @@
                 // we don't need to go through two layout passes when things
                 // change due to fitting system windows, which can happen a lot.
                 windowSizeMayChange |= measureHierarchy(host, lp,
-                        mView.getContext().getResources(),
-                        desiredWindowWidth, desiredWindowHeight);
+                        mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight,
+                        shouldOptimizeMeasure);
             }
         }
 
@@ -3383,6 +3428,13 @@
             maybeHandleWindowMove(frame);
         }
 
+        if (mViewMeasureDeferred) {
+            // It's time to measure the views since we are going to layout them.
+            performMeasure(
+                    MeasureSpec.makeMeasureSpec(frame.width(), MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(frame.height(), MeasureSpec.EXACTLY));
+        }
+
         if (!mRelayoutRequested && mCheckIfCanDraw) {
             // We had a sync previously, but we didn't call IWindowSession#relayout in this
             // traversal. So we don't know if the sync is complete that we can continue to draw.
@@ -3973,6 +4025,9 @@
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIEW);
         }
+        mMeasuredWidth = mView.getMeasuredWidth();
+        mMeasuredHeight = mView.getMeasuredHeight();
+        mViewMeasureDeferred = false;
     }
 
     /**
@@ -4068,7 +4123,7 @@
                         view.requestLayout();
                     }
                     measureHierarchy(host, lp, mView.getContext().getResources(),
-                            desiredWindowWidth, desiredWindowHeight);
+                            desiredWindowWidth, desiredWindowHeight, false /* forRootSizeOnly */);
                     mInLayout = true;
                     host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
 
@@ -8167,8 +8222,8 @@
         final WindowConfiguration winConfigFromWm =
                 mLastReportedMergedConfiguration.getGlobalConfiguration().windowConfiguration;
         final WindowConfiguration winConfig = getCompatWindowConfiguration();
-        final int measuredWidth = mView.getMeasuredWidth();
-        final int measuredHeight = mView.getMeasuredHeight();
+        final int measuredWidth = mMeasuredWidth;
+        final int measuredHeight = mMeasuredHeight;
         final boolean relayoutAsync;
         if (LOCAL_LAYOUT
                 && (mViewFrameInfo.flags & FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) == 0
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 8de15c1..fd55d8d 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -1493,37 +1493,37 @@
         public static String toString(@InsetsType int types) {
             StringBuilder result = new StringBuilder();
             if ((types & STATUS_BARS) != 0) {
-                result.append("statusBars |");
+                result.append("statusBars ");
             }
             if ((types & NAVIGATION_BARS) != 0) {
-                result.append("navigationBars |");
+                result.append("navigationBars ");
             }
             if ((types & CAPTION_BAR) != 0) {
-                result.append("captionBar |");
+                result.append("captionBar ");
             }
             if ((types & IME) != 0) {
-                result.append("ime |");
+                result.append("ime ");
             }
             if ((types & SYSTEM_GESTURES) != 0) {
-                result.append("systemGestures |");
+                result.append("systemGestures ");
             }
             if ((types & MANDATORY_SYSTEM_GESTURES) != 0) {
-                result.append("mandatorySystemGestures |");
+                result.append("mandatorySystemGestures ");
             }
             if ((types & TAPPABLE_ELEMENT) != 0) {
-                result.append("tappableElement |");
+                result.append("tappableElement ");
             }
             if ((types & DISPLAY_CUTOUT) != 0) {
-                result.append("displayCutout |");
+                result.append("displayCutout ");
             }
             if ((types & WINDOW_DECOR) != 0) {
-                result.append("windowDecor |");
+                result.append("windowDecor ");
             }
             if ((types & SYSTEM_OVERLAYS) != 0) {
-                result.append("systemOverlays |");
+                result.append("systemOverlays ");
             }
             if (result.length() > 0) {
-                result.delete(result.length() - 2, result.length());
+                result.delete(result.length() - 1, result.length());
             }
             return result.toString();
         }
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index c067955..b91199d 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -67,6 +67,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -881,7 +882,7 @@
     private long mTraversalBefore = UNDEFINED_NODE_ID;
     private long mTraversalAfter = UNDEFINED_NODE_ID;
 
-    private int mMinMillisBetweenContentChanges;
+    private long mMinDurationBetweenContentChanges = 0;
 
     private int mBooleanProperties;
     private final Rect mBoundsInParent = new Rect();
@@ -1797,18 +1798,21 @@
      * </p>
      *
      * @see AccessibilityEvent#getContentChangeTypes for all content change types.
-     * @param minMillisBetweenContentChanges the minimum duration between content change events.
+     * @param minDurationBetweenContentChanges the minimum duration between content change events.
+     *                                         Negative duration would be treated as zero.
      */
-    public void setMinMillisBetweenContentChanges(int minMillisBetweenContentChanges) {
+    public void setMinDurationBetweenContentChanges(
+            @NonNull Duration minDurationBetweenContentChanges) {
         enforceNotSealed();
-        mMinMillisBetweenContentChanges = minMillisBetweenContentChanges;
+        mMinDurationBetweenContentChanges = minDurationBetweenContentChanges.toMillis();
     }
 
     /**
      * Gets the minimum time duration between two content change events.
      */
-    public int getMinMillisBetweenContentChanges() {
-        return mMinMillisBetweenContentChanges;
+    @NonNull
+    public Duration getMinDurationBetweenContentChanges() {
+        return Duration.ofMillis(mMinDurationBetweenContentChanges);
     }
 
     /**
@@ -4013,8 +4017,8 @@
         fieldIndex++;
         if (mTraversalAfter != DEFAULT.mTraversalAfter) nonDefaultFields |= bitAt(fieldIndex);
         fieldIndex++;
-        if (mMinMillisBetweenContentChanges
-                != DEFAULT.mMinMillisBetweenContentChanges) {
+        if (mMinDurationBetweenContentChanges
+                != DEFAULT.mMinDurationBetweenContentChanges) {
             nonDefaultFields |= bitAt(fieldIndex);
         }
         fieldIndex++;
@@ -4148,7 +4152,7 @@
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalBefore);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalAfter);
         if (isBitSet(nonDefaultFields, fieldIndex++)) {
-            parcel.writeInt(mMinMillisBetweenContentChanges);
+            parcel.writeLong(mMinDurationBetweenContentChanges);
         }
 
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mConnectionId);
@@ -4305,7 +4309,7 @@
         mLabeledById = other.mLabeledById;
         mTraversalBefore = other.mTraversalBefore;
         mTraversalAfter = other.mTraversalAfter;
-        mMinMillisBetweenContentChanges = other.mMinMillisBetweenContentChanges;
+        mMinDurationBetweenContentChanges = other.mMinDurationBetweenContentChanges;
         mWindowId = other.mWindowId;
         mConnectionId = other.mConnectionId;
         mUniqueId = other.mUniqueId;
@@ -4410,7 +4414,7 @@
         if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalBefore = parcel.readLong();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalAfter = parcel.readLong();
         if (isBitSet(nonDefaultFields, fieldIndex++)) {
-            mMinMillisBetweenContentChanges = parcel.readInt();
+            mMinDurationBetweenContentChanges = parcel.readLong();
         }
 
         if (isBitSet(nonDefaultFields, fieldIndex++)) mConnectionId = parcel.readInt();
@@ -4760,8 +4764,8 @@
             builder.append("; mParentNodeId: 0x").append(Long.toHexString(mParentNodeId));
             builder.append("; traversalBefore: 0x").append(Long.toHexString(mTraversalBefore));
             builder.append("; traversalAfter: 0x").append(Long.toHexString(mTraversalAfter));
-            builder.append("; minMillisBetweenContentChanges: ")
-                    .append(mMinMillisBetweenContentChanges);
+            builder.append("; minDurationBetweenContentChanges: ")
+                    .append(mMinDurationBetweenContentChanges);
 
             int granularities = mMovementGranularities;
             builder.append("; MovementGranularities: [");
diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java
index 85b2881..40c0fee 100644
--- a/core/java/android/window/BackEvent.java
+++ b/core/java/android/window/BackEvent.java
@@ -16,29 +16,24 @@
 
 package android.window;
 
+import android.annotation.FloatRange;
 import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.view.RemoteAnimationTarget;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * Represents an event that is sent out by the system during back navigation gesture.
- * Holds information about the touch event, swipe direction and overall progress of the gesture
- * interaction.
- *
- * @hide
+ * Object used to report back gesture progress.
+ * Holds information about the touch event, swipe direction and the animation progress that
+ * predictive back animations should seek to.
  */
-public class BackEvent implements Parcelable {
+public final class BackEvent {
     /** Indicates that the edge swipe starts from the left edge of the screen */
     public static final int EDGE_LEFT = 0;
     /** Indicates that the edge swipe starts from the right edge of the screen */
     public static final int EDGE_RIGHT = 1;
 
+    /** @hide */
     @IntDef({
             EDGE_LEFT,
             EDGE_RIGHT,
@@ -52,78 +47,52 @@
 
     @SwipeEdge
     private final int mSwipeEdge;
-    @Nullable
-    private final RemoteAnimationTarget mDepartingAnimationTarget;
 
     /**
-     * Creates a new {@link BackEvent} instance.
+     * Creates a new {@link BackMotionEvent} instance.
      *
      * @param touchX Absolute X location of the touch point of this event.
      * @param touchY Absolute Y location of the touch point of this event.
      * @param progress Value between 0 and 1 on how far along the back gesture is.
      * @param swipeEdge Indicates which edge the swipe starts from.
-     * @param departingAnimationTarget The remote animation target of the departing
-     *                                 application window.
      */
-    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge,
-            @Nullable RemoteAnimationTarget departingAnimationTarget) {
+    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge) {
         mTouchX = touchX;
         mTouchY = touchY;
         mProgress = progress;
         mSwipeEdge = swipeEdge;
-        mDepartingAnimationTarget = departingAnimationTarget;
-    }
-
-    private BackEvent(@NonNull Parcel in) {
-        mTouchX = in.readFloat();
-        mTouchY = in.readFloat();
-        mProgress = in.readFloat();
-        mSwipeEdge = in.readInt();
-        mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
-    }
-
-    public static final Creator<BackEvent> CREATOR = new Creator<BackEvent>() {
-        @Override
-        public BackEvent createFromParcel(Parcel in) {
-            return new BackEvent(in);
-        }
-
-        @Override
-        public BackEvent[] newArray(int size) {
-            return new BackEvent[size];
-        }
-    };
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeFloat(mTouchX);
-        dest.writeFloat(mTouchY);
-        dest.writeFloat(mProgress);
-        dest.writeInt(mSwipeEdge);
-        dest.writeTypedObject(mDepartingAnimationTarget, flags);
     }
 
     /**
-     * Returns a value between 0 and 1 on how far along the back gesture is.
+     * Returns a value between 0 and 1 on how far along the back gesture is. This value is
+     * driven by the horizontal location of the touch point, and should be used as the fraction to
+     * seek the predictive back animation with. Specifically,
+     * <ol>
+     * <li>The progress is 0 when the touch is at the starting edge of the screen (left or right),
+     * and animation should seek to its start state.
+     * <li>The progress is approximately 1 when the touch is at the opposite side of the screen,
+     * and animation should seek to its end state. Exact end value may vary depending on
+     * screen size.
+     * </ol>
+     * In-between locations are linearly interpolated based on horizontal distance from the starting
+     * edge and smooth clamped to 1 when the distance exceeds a system-wide threshold.
      */
+    @FloatRange(from = 0, to = 1)
     public float getProgress() {
         return mProgress;
     }
 
     /**
-     * Returns the absolute X location of the touch point.
+     * Returns the absolute X location of the touch point, or NaN if the event is from
+     * a button press.
      */
     public float getTouchX() {
         return mTouchX;
     }
 
     /**
-     * Returns the absolute Y location of the touch point.
+     * Returns the absolute Y location of the touch point, or NaN if the event is from
+     * a button press.
      */
     public float getTouchY() {
         return mTouchY;
@@ -132,20 +101,11 @@
     /**
      * Returns the screen edge that the swipe starts from.
      */
+    @SwipeEdge
     public int getSwipeEdge() {
         return mSwipeEdge;
     }
 
-    /**
-     * Returns the {@link RemoteAnimationTarget} of the top departing application window,
-     * or {@code null} if the top window should not be moved for the current type of back
-     * destination.
-     */
-    @Nullable
-    public RemoteAnimationTarget getDepartingAnimationTarget() {
-        return mDepartingAnimationTarget;
-    }
-
     @Override
     public String toString() {
         return "BackEvent{"
diff --git a/core/java/android/window/BackEvent.aidl b/core/java/android/window/BackMotionEvent.aidl
similarity index 95%
rename from core/java/android/window/BackEvent.aidl
rename to core/java/android/window/BackMotionEvent.aidl
index 821f1fa..7c675c3 100644
--- a/core/java/android/window/BackEvent.aidl
+++ b/core/java/android/window/BackMotionEvent.aidl
@@ -19,4 +19,4 @@
 /**
  * @hide
  */
-parcelable BackEvent;
+parcelable BackMotionEvent;
diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java
new file mode 100644
index 0000000..8012a1c
--- /dev/null
+++ b/core/java/android/window/BackMotionEvent.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.RemoteAnimationTarget;
+
+/**
+ * Object used to report back gesture progress. Holds information about a {@link BackEvent} plus
+ * any {@link RemoteAnimationTarget} the gesture manipulates.
+ *
+ * @see BackEvent
+ * @hide
+ */
+public final class BackMotionEvent implements Parcelable {
+    private final float mTouchX;
+    private final float mTouchY;
+    private final float mProgress;
+
+    @BackEvent.SwipeEdge
+    private final int mSwipeEdge;
+    @Nullable
+    private final RemoteAnimationTarget mDepartingAnimationTarget;
+
+    /**
+     * Creates a new {@link BackMotionEvent} instance.
+     *
+     * @param touchX Absolute X location of the touch point of this event.
+     * @param touchY Absolute Y location of the touch point of this event.
+     * @param progress Value between 0 and 1 on how far along the back gesture is.
+     * @param swipeEdge Indicates which edge the swipe starts from.
+     * @param departingAnimationTarget The remote animation target of the departing
+     *                                 application window.
+     */
+    public BackMotionEvent(float touchX, float touchY, float progress,
+            @BackEvent.SwipeEdge int swipeEdge,
+            @Nullable RemoteAnimationTarget departingAnimationTarget) {
+        mTouchX = touchX;
+        mTouchY = touchY;
+        mProgress = progress;
+        mSwipeEdge = swipeEdge;
+        mDepartingAnimationTarget = departingAnimationTarget;
+    }
+
+    private BackMotionEvent(@NonNull Parcel in) {
+        mTouchX = in.readFloat();
+        mTouchY = in.readFloat();
+        mProgress = in.readFloat();
+        mSwipeEdge = in.readInt();
+        mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
+    }
+
+    @NonNull
+    public static final Creator<BackMotionEvent> CREATOR = new Creator<BackMotionEvent>() {
+        @Override
+        public BackMotionEvent createFromParcel(Parcel in) {
+            return new BackMotionEvent(in);
+        }
+
+        @Override
+        public BackMotionEvent[] newArray(int size) {
+            return new BackMotionEvent[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeFloat(mTouchX);
+        dest.writeFloat(mTouchY);
+        dest.writeFloat(mProgress);
+        dest.writeInt(mSwipeEdge);
+        dest.writeTypedObject(mDepartingAnimationTarget, flags);
+    }
+
+    /**
+     * Returns the progress of a {@link BackEvent}.
+     *
+     * @see BackEvent#getProgress()
+     */
+    @FloatRange(from = 0, to = 1)
+    public float getProgress() {
+        return mProgress;
+    }
+
+    /**
+     * Returns the absolute X location of the touch point.
+     */
+    public float getTouchX() {
+        return mTouchX;
+    }
+
+    /**
+     * Returns the absolute Y location of the touch point.
+     */
+    public float getTouchY() {
+        return mTouchY;
+    }
+
+    /**
+     * Returns the screen edge that the swipe starts from.
+     */
+    @BackEvent.SwipeEdge
+    public int getSwipeEdge() {
+        return mSwipeEdge;
+    }
+
+    /**
+     * Returns the {@link RemoteAnimationTarget} of the top departing application window,
+     * or {@code null} if the top window should not be moved for the current type of back
+     * destination.
+     */
+    @Nullable
+    public RemoteAnimationTarget getDepartingAnimationTarget() {
+        return mDepartingAnimationTarget;
+    }
+
+    @Override
+    public String toString() {
+        return "BackMotionEvent{"
+                + "mTouchX=" + mTouchX
+                + ", mTouchY=" + mTouchY
+                + ", mProgress=" + mProgress
+                + ", mSwipeEdge" + mSwipeEdge
+                + ", mDepartingAnimationTarget" + mDepartingAnimationTarget
+                + "}";
+    }
+}
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
index 2e3afde..14a57e0 100644
--- a/core/java/android/window/BackProgressAnimator.java
+++ b/core/java/android/window/BackProgressAnimator.java
@@ -40,7 +40,7 @@
     private final SpringAnimation mSpring;
     private ProgressCallback mCallback;
     private float mProgress = 0;
-    private BackEvent mLastBackEvent;
+    private BackMotionEvent mLastBackEvent;
     private boolean mStarted = false;
 
     private void setProgress(float progress) {
@@ -82,9 +82,9 @@
     /**
      * Sets a new target position for the back progress.
      *
-     * @param event the {@link BackEvent} containing the latest target progress.
+     * @param event the {@link BackMotionEvent} containing the latest target progress.
      */
-    public void onBackProgressed(BackEvent event) {
+    public void onBackProgressed(BackMotionEvent event) {
         if (!mStarted) {
             return;
         }
@@ -98,11 +98,11 @@
     /**
      * Starts the back progress animation.
      *
-     * @param event the {@link BackEvent} that started the gesture.
+     * @param event the {@link BackMotionEvent} that started the gesture.
      * @param callback the back callback to invoke for the gesture. It will receive back progress
      *                 dispatches as the progress animation updates.
      */
-    public void onBackStarted(BackEvent event, ProgressCallback callback) {
+    public void onBackStarted(BackMotionEvent event, ProgressCallback callback) {
         reset();
         mLastBackEvent = event;
         mCallback = callback;
@@ -132,8 +132,7 @@
         }
         mCallback.onProgressUpdate(
                 new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(),
-                        progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(),
-                        mLastBackEvent.getDepartingAnimationTarget()));
+                        progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge()));
     }
 
 }
diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java
index f55932e..e027934 100644
--- a/core/java/android/window/DisplayWindowPolicyController.java
+++ b/core/java/android/window/DisplayWindowPolicyController.java
@@ -128,9 +128,10 @@
             ActivityInfo activityInfo, int windowFlags, int systemWindowFlags);
 
     /**
-     * Returns {@code true} if the tasks which is on this virtual display can be showed on Recents.
+     * Returns {@code true} if the tasks which is on this virtual display can be showed in the
+     * host device of the recently launched activities list.
      */
-    public abstract boolean canShowTasksInRecents();
+    public abstract boolean canShowTasksInHostDeviceRecents();
 
     /**
      * This is called when the top activity of the display is changed.
diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl
index 6af8ddd..159c0e8 100644
--- a/core/java/android/window/IOnBackInvokedCallback.aidl
+++ b/core/java/android/window/IOnBackInvokedCallback.aidl
@@ -17,7 +17,7 @@
 
 package android.window;
 
-import android.window.BackEvent;
+import android.window.BackMotionEvent;
 
 /**
  * Interface that wraps a {@link OnBackInvokedCallback} object, to be stored in window manager
@@ -30,18 +30,19 @@
     * Called when a back gesture has been started, or back button has been pressed down.
     * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}.
     *
-    * @param backEvent The {@link BackEvent} containing information about the touch or button press.
+    * @param backMotionEvent The {@link BackMotionEvent} containing information about the touch
+    *        or button press.
     */
-    void onBackStarted(in BackEvent backEvent);
+    void onBackStarted(in BackMotionEvent backMotionEvent);
 
     /**
      * Called on back gesture progress.
      * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}.
      *
-     * @param backEvent The {@link BackEvent} containing information about the latest touch point
-     *                  and the progress that the back animation should seek to.
+     * @param backMotionEvent The {@link BackMotionEvent} containing information about the latest
+     *                        touch point and the progress that the back animation should seek to.
      */
-    void onBackProgressed(in BackEvent backEvent);
+    void onBackProgressed(in BackMotionEvent backMotionEvent);
 
     /**
      * Called when a back gesture or back button press has been cancelled.
diff --git a/core/java/android/window/OnBackAnimationCallback.java b/core/java/android/window/OnBackAnimationCallback.java
index c05809b..9119e71 100644
--- a/core/java/android/window/OnBackAnimationCallback.java
+++ b/core/java/android/window/OnBackAnimationCallback.java
@@ -18,6 +18,8 @@
 import android.app.Activity;
 import android.app.Dialog;
 import android.view.View;
+import android.view.Window;
+
 /**
  * Interface for applications to register back animation callbacks along their custom back
  * handling.
@@ -25,24 +27,29 @@
  * This allows the client to customize various back behaviors by overriding the corresponding
  * callback methods.
  * <p>
- * Callback instances can be added to and removed from {@link OnBackInvokedDispatcher}, held
- * by classes that implement {@link OnBackInvokedDispatcherOwner} (such as {@link Activity},
- * {@link Dialog} and {@link View}).
+ * Callback instances can be added to and removed from {@link OnBackInvokedDispatcher}, which
+ * is held at window level and accessible through {@link Activity#getOnBackInvokedDispatcher()},
+ * {@link Dialog#getOnBackInvokedDispatcher()}, {@link Window#getOnBackInvokedDispatcher()}
+ * and {@link View#findOnBackInvokedDispatcher()}.
  * <p>
  * When back is triggered, callbacks on the in-focus window are invoked in reverse order in which
  * they are added within the same priority. Between different priorities, callbacks with higher
  * priority are invoked first.
  * <p>
  * @see OnBackInvokedCallback
- * @hide
  */
 public interface OnBackAnimationCallback extends OnBackInvokedCallback {
     /**
      * Called when a back gesture has been started, or back button has been pressed down.
+     *
+     * @param backEvent The {@link BackEvent} containing information about the touch or
+     *                  button press.
+     * @see BackEvent
      */
-    default void onBackStarted() { }
+    default void onBackStarted(@NonNull BackEvent backEvent) {}
+
     /**
-     * Called on back gesture progress.
+     * Called when a back gesture progresses.
      *
      * @param backEvent An {@link BackEvent} object describing the progress event.
      *
diff --git a/core/java/android/window/OnBackInvokedCallback.java b/core/java/android/window/OnBackInvokedCallback.java
index 62c41bf..6beaad3 100644
--- a/core/java/android/window/OnBackInvokedCallback.java
+++ b/core/java/android/window/OnBackInvokedCallback.java
@@ -16,9 +16,9 @@
 
 package android.window;
 
-import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.Dialog;
+import android.view.View;
 import android.view.Window;
 
 /**
@@ -26,7 +26,8 @@
  * <p>
  * Callback instances can be added to and removed from {@link OnBackInvokedDispatcher}, which
  * is held at window level and accessible through {@link Activity#getOnBackInvokedDispatcher()},
- * {@link Dialog#getOnBackInvokedDispatcher()} and {@link Window#getOnBackInvokedDispatcher()}.
+ * {@link Dialog#getOnBackInvokedDispatcher()}, {@link Window#getOnBackInvokedDispatcher()}
+ * and {@link View#findOnBackInvokedDispatcher()}.
  * <p>
  * When back is triggered, callbacks on the in-focus window are invoked in reverse order in which
  * they are added within the same priority. Between different priorities, callbacks with higher
@@ -35,6 +36,9 @@
  * This replaces {@link Activity#onBackPressed()}, {@link Dialog#onBackPressed()} and
  * {@link android.view.KeyEvent#KEYCODE_BACK}
  * <p>
+ * If you want to customize back animation behaviors, in addition to handling back invocations,
+ * register its subclass instances {@link OnBackAnimationCallback} instead.
+ * <p>
  * @see OnBackInvokedDispatcher#registerOnBackInvokedCallback(int, OnBackInvokedCallback)
  * registerOnBackInvokedCallback(priority, OnBackInvokedCallback)
  * to specify callback priority.
@@ -42,35 +46,8 @@
 @SuppressWarnings("deprecation")
 public interface OnBackInvokedCallback {
     /**
-     * Called when a back gesture has been started, or back button has been pressed down.
-     *
-     * @param backEvent The {@link BackEvent} containing information about the touch or
-     *                  button press.
-     *
-     * @hide
-     */
-    default void onBackStarted(@NonNull BackEvent backEvent) {}
-
-    /**
-     * Called when a back gesture has been progressed.
-     *
-     * @param backEvent The {@link BackEvent} containing information about the latest touch point
-     *                  and the progress that the back animation should seek to.
-     *
-     * @hide
-     */
-    default void onBackProgressed(@NonNull BackEvent backEvent) {}
-
-    /**
      * Called when a back gesture has been completed and committed, or back button pressed
      * has been released and committed.
      */
     void onBackInvoked();
-
-    /**
-     * Called when a back gesture or button press has been cancelled.
-     *
-     * @hide
-     */
-    default void onBackCancelled() {}
 }
diff --git a/core/java/android/window/WindowContainerToken.java b/core/java/android/window/WindowContainerToken.java
index 22b90b2..b914cb0 100644
--- a/core/java/android/window/WindowContainerToken.java
+++ b/core/java/android/window/WindowContainerToken.java
@@ -48,7 +48,6 @@
     }
 
     @Override
-    /** @hide */
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeStrongBinder(mRealToken.asBinder());
     }
@@ -68,7 +67,6 @@
             };
 
     @Override
-    /** @hide */
     public int describeContents() {
         return 0;
     }
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 1063532..5793674 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -827,6 +827,26 @@
     }
 
     /**
+     * Sets/removes the reparent leaf task flag for this {@code windowContainer}.
+     * When this is set, the server side will try to reparent the leaf task to task display area
+     * if there is an existing activity in history during the activity launch. This operation only
+     * support on the organized root task.
+     * @hide
+     */
+    @NonNull
+    public WindowContainerTransaction setReparentLeafTaskIfRelaunch(
+            @NonNull WindowContainerToken windowContainer, boolean reparentLeafTaskIfRelaunch) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(
+                        HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH)
+                        .setContainer(windowContainer.asBinder())
+                        .setReparentLeafTaskIfRelaunch(reparentLeafTaskIfRelaunch)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
      * Merges another WCT into this one.
      * @param transfer When true, this will transfer everything from other potentially leaving
      *                 other in an unusable state. When false, other is left alone, but
@@ -905,7 +925,6 @@
     }
 
     @Override
-    /** @hide */
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeMap(mChanges);
         dest.writeTypedList(mHierarchyOps);
@@ -914,7 +933,6 @@
     }
 
     @Override
-    /** @hide */
     public int describeContents() {
         return 0;
     }
@@ -1242,6 +1260,7 @@
         public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 21;
         public static final int HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 22;
         public static final int HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS = 23;
+        public static final int HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH = 24;
 
         // The following key(s) are for use with mLaunchOptions:
         // When launching a task (eg. from recents), this is the taskId to be launched.
@@ -1294,6 +1313,8 @@
 
         private boolean mAlwaysOnTop;
 
+        private boolean mReparentLeafTaskIfRelaunch;
+
         public static HierarchyOp createForReparent(
                 @NonNull IBinder container, @Nullable IBinder reparent, boolean toTop) {
             return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_REPARENT)
@@ -1406,6 +1427,7 @@
             mPendingIntent = copy.mPendingIntent;
             mShortcutInfo = copy.mShortcutInfo;
             mAlwaysOnTop = copy.mAlwaysOnTop;
+            mReparentLeafTaskIfRelaunch = copy.mReparentLeafTaskIfRelaunch;
         }
 
         protected HierarchyOp(Parcel in) {
@@ -1428,6 +1450,7 @@
             mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
             mShortcutInfo = in.readTypedObject(ShortcutInfo.CREATOR);
             mAlwaysOnTop = in.readBoolean();
+            mReparentLeafTaskIfRelaunch = in.readBoolean();
         }
 
         public int getType() {
@@ -1502,6 +1525,10 @@
             return mAlwaysOnTop;
         }
 
+        public boolean isReparentLeafTaskIfRelaunch() {
+            return mReparentLeafTaskIfRelaunch;
+        }
+
         @Nullable
         public TaskFragmentCreationParams getTaskFragmentCreationOptions() {
             return mTaskFragmentCreationOptions;
@@ -1582,6 +1609,9 @@
                             + mReparent + "}";
                 case HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS:
                     return "{ClearAdjacentRoot: container=" + mContainer + "}";
+                case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH:
+                    return "{setReparentLeafTaskIfRelaunch: container= " + mContainer
+                            + " reparentLeafTaskIfRelaunch= " + mReparentLeafTaskIfRelaunch + "}";
                 default:
                     return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent
                             + " mToTop=" + mToTop
@@ -1612,6 +1642,7 @@
             dest.writeTypedObject(mPendingIntent, flags);
             dest.writeTypedObject(mShortcutInfo, flags);
             dest.writeBoolean(mAlwaysOnTop);
+            dest.writeBoolean(mReparentLeafTaskIfRelaunch);
         }
 
         @Override
@@ -1672,6 +1703,8 @@
 
             private boolean mAlwaysOnTop;
 
+            private boolean mReparentLeafTaskIfRelaunch;
+
             Builder(int type) {
                 mType = type;
             }
@@ -1742,6 +1775,11 @@
                 return this;
             }
 
+            Builder setReparentLeafTaskIfRelaunch(boolean reparentLeafTaskIfRelaunch) {
+                mReparentLeafTaskIfRelaunch = reparentLeafTaskIfRelaunch;
+                return this;
+            }
+
             Builder setShortcutInfo(@Nullable ShortcutInfo shortcutInfo) {
                 mShortcutInfo = shortcutInfo;
                 return this;
@@ -1767,6 +1805,7 @@
                 hierarchyOp.mAlwaysOnTop = mAlwaysOnTop;
                 hierarchyOp.mTaskFragmentCreationOptions = mTaskFragmentCreationOptions;
                 hierarchyOp.mShortcutInfo = mShortcutInfo;
+                hierarchyOp.mReparentLeafTaskIfRelaunch = mReparentLeafTaskIfRelaunch;
 
                 return hierarchyOp;
             }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index fda39c1..dd9483a 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -229,19 +229,21 @@
         }
 
         @Override
-        public void onBackStarted(BackEvent backEvent) {
+        public void onBackStarted(BackMotionEvent backEvent) {
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
                     mProgressAnimator.onBackStarted(backEvent, event ->
                             callback.onBackProgressed(event));
-                    callback.onBackStarted(backEvent);
+                    callback.onBackStarted(new BackEvent(
+                            backEvent.getTouchX(), backEvent.getTouchY(),
+                            backEvent.getProgress(), backEvent.getSwipeEdge()));
                 }
             });
         }
 
         @Override
-        public void onBackProgressed(BackEvent backEvent) {
+        public void onBackProgressed(BackMotionEvent backEvent) {
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 614f962..75f0bf5 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -97,6 +97,7 @@
 import android.os.HandlerExecutor;
 import android.os.HandlerThread;
 import android.provider.DeviceConfig;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
@@ -415,7 +416,7 @@
     @VisibleForTesting
     public InteractionJankMonitor(@NonNull HandlerThread worker) {
         // Check permission early.
-        DeviceConfig.enforceReadPermission(
+        Settings.Config.enforceReadPermission(
             DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR);
 
         mRunningTrackers = new SparseArray<>();
diff --git a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
index a182920..2748ded 100644
--- a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
+++ b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
@@ -24,6 +24,7 @@
 import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__EXECUTING_SERVICE;
 import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__INPUT_DISPATCHING_TIMEOUT;
 import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__INPUT_DISPATCHING_TIMEOUT_NO_FOCUSED_WINDOW;
+import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__SHORT_FGS_TIMEOUT;
 import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__START_FOREGROUND_SERVICE;
 import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__UNKNOWN_ANR_TYPE;
 
@@ -400,6 +401,8 @@
                 return ANRLATENCY_REPORTED__ANR_TYPE__EXECUTING_SERVICE;
             case TimeoutKind.CONTENT_PROVIDER:
                 return ANRLATENCY_REPORTED__ANR_TYPE__CONTENT_PROVIDER_NOT_RESPONDING;
+            case TimeoutKind.SHORT_FGS_TIMEOUT:
+                return ANRLATENCY_REPORTED__ANR_TYPE__SHORT_FGS_TIMEOUT;
             default:
                 return ANRLATENCY_REPORTED__ANR_TYPE__UNKNOWN_ANR_TYPE;
         }
diff --git a/core/java/com/android/internal/power/ModemPowerProfile.java b/core/java/com/android/internal/power/ModemPowerProfile.java
index afea69a..a555ae3 100644
--- a/core/java/com/android/internal/power/ModemPowerProfile.java
+++ b/core/java/com/android/internal/power/ModemPowerProfile.java
@@ -158,11 +158,11 @@
 
     private static final SparseArray<String> MODEM_TX_LEVEL_NAMES = new SparseArray<>(5);
     static {
-        MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_0, "0");
-        MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_1, "1");
-        MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_2, "2");
-        MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_3, "3");
-        MODEM_DRAIN_TYPE_NAMES.put(MODEM_TX_LEVEL_4, "4");
+        MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_0, "0");
+        MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_1, "1");
+        MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_2, "2");
+        MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_3, "3");
+        MODEM_TX_LEVEL_NAMES.put(MODEM_TX_LEVEL_4, "4");
     }
 
     private static final int[] MODEM_TX_LEVEL_MAP = new int[]{
@@ -418,7 +418,10 @@
         return Double.NaN;
     }
 
-    private static String keyToString(int key) {
+    /**
+     * Returns a human readable version of a key.
+     */
+    public static String keyToString(int key) {
         StringBuilder sb = new StringBuilder();
         final int drainType = key & MODEM_DRAIN_TYPE_MASK;
         appendFieldToString(sb, "drain", MODEM_DRAIN_TYPE_NAMES, drainType);
@@ -427,6 +430,7 @@
         if (drainType == MODEM_DRAIN_TYPE_TX) {
             final int txLevel = key & MODEM_TX_LEVEL_MASK;
             appendFieldToString(sb, "level", MODEM_TX_LEVEL_NAMES, txLevel);
+            sb.append(",");
         }
 
         final int ratType = key & MODEM_RAT_TYPE_MASK;
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index ebfc4f7..db288c0 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -327,4 +327,14 @@
 
     /** Shows rear display educational dialog */
     void showRearDisplayDialog(int currentBaseState);
+
+    /** Called when requested to go to fullscreen from the active split app. */
+    void goToFullscreenFromSplit();
+
+    /**
+     * Enters stage split from a current running app.
+     *
+     * @param leftOrTop indicates where the stage split is.
+     */
+    void enterStageSplitFromRunningApp(boolean leftOrTop);
 }
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 1bc903a..a43f0b3 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -339,6 +339,9 @@
                 "dnsproxyd_protocol_headers",
                 "libtextclassifier_hash_headers",
             ],
+            runtime_libs: [
+                "libidmap2",
+            ],
         },
         host: {
             cflags: [
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index 019e3bd..a8d8a43 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -80,7 +80,7 @@
                        VsyncEventData vsyncEventData) override;
     void dispatchHotplug(nsecs_t timestamp, PhysicalDisplayId displayId, bool connected) override;
     void dispatchModeChanged(nsecs_t timestamp, PhysicalDisplayId displayId, int32_t modeId,
-                             nsecs_t vsyncPeriod) override;
+                             nsecs_t renderPeriod) override;
     void dispatchFrameRateOverrides(nsecs_t timestamp, PhysicalDisplayId displayId,
                                     std::vector<FrameRateOverride> overrides) override;
     void dispatchNullEvent(nsecs_t timestamp, PhysicalDisplayId displayId) override {}
@@ -168,14 +168,14 @@
 }
 
 void NativeDisplayEventReceiver::dispatchModeChanged(nsecs_t timestamp, PhysicalDisplayId displayId,
-                                                     int32_t modeId, nsecs_t) {
+                                                     int32_t modeId, nsecs_t renderPeriod) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
 
     ScopedLocalRef<jobject> receiverObj(env, GetReferent(env, mReceiverWeakGlobal));
     if (receiverObj.get()) {
         ALOGV("receiver %p ~ Invoking mode changed handler.", this);
         env->CallVoidMethod(receiverObj.get(), gDisplayEventReceiverClassInfo.dispatchModeChanged,
-                            timestamp, displayId.value, modeId);
+                            timestamp, displayId.value, modeId, renderPeriod);
         ALOGV("receiver %p ~ Returned from mode changed handler.", this);
     }
 
@@ -290,7 +290,7 @@
             gDisplayEventReceiverClassInfo.clazz, "dispatchHotplug", "(JJZ)V");
     gDisplayEventReceiverClassInfo.dispatchModeChanged =
             GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchModeChanged",
-                             "(JJI)V");
+                             "(JJIJ)V");
     gDisplayEventReceiverClassInfo.dispatchFrameRateOverrides =
             GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz,
                              "dispatchFrameRateOverrides",
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 2c5386d..1ed3555 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -109,6 +109,7 @@
     jmethodID ctor;
     jfieldID supportedDisplayModes;
     jfieldID activeDisplayModeId;
+    jfieldID renderFrameRate;
     jfieldID supportedColorModes;
     jfieldID activeColorMode;
     jfieldID hdrCapabilities;
@@ -1184,6 +1185,7 @@
     env->SetObjectField(object, gDynamicDisplayInfoClassInfo.supportedDisplayModes, modesArray);
     env->SetIntField(object, gDynamicDisplayInfoClassInfo.activeDisplayModeId,
                      info.activeDisplayModeId);
+    env->SetFloatField(object, gDynamicDisplayInfoClassInfo.renderFrameRate, info.renderFrameRate);
 
     jintArray colorModesArray = env->NewIntArray(info.supportedColorModes.size());
     if (colorModesArray == NULL) {
@@ -2174,6 +2176,8 @@
                             "[Landroid/view/SurfaceControl$DisplayMode;");
     gDynamicDisplayInfoClassInfo.activeDisplayModeId =
             GetFieldIDOrDie(env, dynamicInfoClazz, "activeDisplayModeId", "I");
+    gDynamicDisplayInfoClassInfo.renderFrameRate =
+            GetFieldIDOrDie(env, dynamicInfoClazz, "renderFrameRate", "F");
     gDynamicDisplayInfoClassInfo.supportedColorModes =
             GetFieldIDOrDie(env, dynamicInfoClazz, "supportedColorModes", "[I");
     gDynamicDisplayInfoClassInfo.activeColorMode =
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index 7393c6f..18d84d5 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -457,6 +457,7 @@
         optional bool stop_if_killed = 3;
         optional bool call_start = 4;
         optional int32 last_start_id = 5;
+        optional int32 start_command_result = 6;
     }
     optional Start start = 19;
 
@@ -499,7 +500,21 @@
     repeated ConnectionRecordProto connections = 26;
 
     optional bool allow_while_in_use_permission_in_fgs = 27;
-    // Next Tag: 28
+
+    message ShortFgsInfo {
+        option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+        optional int64 start_time = 1;
+        optional int32 start_foreground_count = 2;
+        optional int32 start_id = 3;
+        optional int64 timeout_time = 4;
+        optional int64 proc_state_demote_time = 5;
+        optional int64 anr_time = 6;
+    }
+
+    optional ShortFgsInfo short_fgs_info = 28;
+
+    // Next Tag: 29
 }
 
 message ConnectionRecordProto {
diff --git a/core/res/Android.bp b/core/res/Android.bp
index 7e17840..3c4bac8 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -107,7 +107,9 @@
     sdk_version: "core_platform",
     certificate: "platform",
 
-    srcs: [":remote-color-resources-arsc"],
+    srcs: [
+        ":remote-color-resources-arsc",
+    ],
 
     // Disable dexpreopt and verify_uses_libraries check as the app
     // contains no Java code to be dexpreopted.
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ad8f7fb..fb451dd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3327,7 +3327,9 @@
          <p>Protection level: normal
          -->
     <permission android:name="android.permission.REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND"
-        android:protectionLevel="normal"/>
+                android:label="@string/permlab_startForegroundServicesFromBackground"
+                android:description="@string/permdesc_startForegroundServicesFromBackground"
+                android:protectionLevel="normal"/>
 
     <!-- Allows a companion app to use data in the background.
          <p>Protection level: normal
@@ -3343,6 +3345,8 @@
          <p>Protection level: normal
      -->
     <permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH"
+                android:label="@string/permlab_companionProfileWatch"
+                android:description="@string/permdesc_companionProfileWatch"
                 android:protectionLevel="normal" />
 
     <!-- Allows application to request to be associated with a virtual display capable of streaming
@@ -4914,11 +4918,15 @@
          of their associated companion device
          -->
     <permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE"
+                android:label="@string/permlab_observeCompanionDevicePresence"
+                android:description="@string/permdesc_observeCompanionDevicePresence"
                 android:protectionLevel="normal" />
 
     <!-- Allows an application to deliver companion messages to system
          -->
     <permission android:name="android.permission.DELIVER_COMPANION_MESSAGES"
+                android:label="@string/permlab_deliverCompanionMessages"
+                android:description="@string/permdesc_deliverCompanionMessages"
                 android:protectionLevel="normal" />
 
     <!-- Allows an application to create new companion device associations.
@@ -6286,6 +6294,15 @@
         android:protectionLevel="normal|instant" />
 
     <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "fileManagement".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT"
+        android:description="@string/permdesc_foregroundServiceFileManagement"
+        android:label="@string/permlab_foregroundServiceFileManagement"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
          Service.startForeground} with the type "specialUse".
          <p>Protection level: normal|appop|instant
     -->
diff --git a/core/res/assets/geoid_height_map/README.md b/core/res/assets/geoid_height_map/README.md
new file mode 100644
index 0000000..849d32e
--- /dev/null
+++ b/core/res/assets/geoid_height_map/README.md
@@ -0,0 +1,2 @@
+These binary protos are generated at runtime from the text protos in ../../geoid_height_map_assets
+and using aprotoc.
\ No newline at end of file
diff --git a/core/res/assets/geoid_height_map/map-params.pb b/core/res/assets/geoid_height_map/map-params.pb
new file mode 100644
index 0000000..6fd4022
--- /dev/null
+++ b/core/res/assets/geoid_height_map/map-params.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-1.pb b/core/res/assets/geoid_height_map/tile-1.pb
new file mode 100644
index 0000000..546dd0d
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-1.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-3.pb b/core/res/assets/geoid_height_map/tile-3.pb
new file mode 100644
index 0000000..eb3fe46
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-3.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-5.pb b/core/res/assets/geoid_height_map/tile-5.pb
new file mode 100644
index 0000000..0243d6d0
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-5.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-7.pb b/core/res/assets/geoid_height_map/tile-7.pb
new file mode 100644
index 0000000..3c2f777
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-7.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-9.pb b/core/res/assets/geoid_height_map/tile-9.pb
new file mode 100644
index 0000000..5e9a480
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-9.pb
Binary files differ
diff --git a/core/res/assets/geoid_height_map/tile-b.pb b/core/res/assets/geoid_height_map/tile-b.pb
new file mode 100644
index 0000000..c57e873
--- /dev/null
+++ b/core/res/assets/geoid_height_map/tile-b.pb
Binary files differ
diff --git a/core/res/geoid_height_map_assets/README.md b/core/res/geoid_height_map_assets/README.md
new file mode 100644
index 0000000..800b3e5
--- /dev/null
+++ b/core/res/geoid_height_map_assets/README.md
@@ -0,0 +1,8 @@
+These text protos contain composite JPEG/PNG images representing the EGM2008 Earth Gravitational
+Model[^1] published by the National Geospatial-Intelligence Agency.[^2]
+
+[^1]: Pavlis, Nikolaos K., et al. "The development and evaluation of the Earth Gravitational Model
+2008 (EGM2008)." Journal of geophysical research: solid earth 117.B4 (2012).
+
+[^2]: National Geospatial-Intelligence Agency. “Office of Geomatics.” 2022.
+URL: https://earth-info.nga.mil.
\ No newline at end of file
diff --git a/core/res/geoid_height_map_assets/map-params.textpb b/core/res/geoid_height_map_assets/map-params.textpb
new file mode 100644
index 0000000..3f504d4
--- /dev/null
+++ b/core/res/geoid_height_map_assets/map-params.textpb
@@ -0,0 +1,6 @@
+map_s2_level: 9
+cache_tile_s2_level: 5
+disk_tile_s2_level: 0
+model_a_meters: 255.0
+model_b_meters: -128.0
+model_rmse_meters: 0.36
diff --git a/core/res/geoid_height_map_assets/tile-1.textpb b/core/res/geoid_height_map_assets/tile-1.textpb
new file mode 100644
index 0000000..7fac234
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-1.textpb
@@ -0,0 +1,3 @@
+tile_key: "1"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\244i(\242\224R\216\264\361OZ\221je\251V\246Z\225jE\251V\245Z\225jE\247\216\324\361N\317\024f\214\322\023I\2323I\232L\322f\220\232LdqM\316:\321E\024\354\344sJ)\364\340i\340S\200\315(\030\243\255:\224\032p4\264\340i\300\324\213\315:\232EFz\320\0174\244\323I\246\223M-F\3527R\026\244\rR+S\353\314\r%\024QN\247\212z\324\253R\255J\2652\324\253R\255H*U\251\024\324\213O\006\244\024\264QM4RRRt\244$\342\220{\322a\201\312\363M,I\344P\017\255;\214\360h\245\006\234\r<5(4\365cO\006\226\227\024\264\nPiisNSS)\245&\232M0\232J3Fj65\031j7Rn\244\335F\352\225\033\212\224\036+\314\215!8\242\212)\303\245<S\326\245Z\225jU\251\226\245Z\221jU5\"\324\202\244\006\236)\342\235\2323HNh\2434\204\322SI\244\3154\322\t\n\364\241e\301\346\234]X\360(\371{Ss\315(4\340i\340\323\305<S\251A\365\247\216zQE.i3J\017\255(8\251U\251\305\251\204\323wRg4f\226\243sQ\023M\335I\272\223u\033\252x\232\236\322W\232\346\201\315)\351H\r-8S\305<\n\221jU\251V\246Z\225MJ\265\"\232\221jQO\024\360i\340\323\263I\232;\321\232Bi3IHO\024\302i3M4\334R\216)E-:\214\323\301\251\024\323\301\247\203K\326\201\221\305(4\273\251C\003KE&j@iwSKSKR\006\247f\226\230\325\003\034Te\251\245\250\335K\272\244G\300\241\236\274\374\212J\t\245\024\264\341O\002\244\025 \251TT\252*Q\305H\246\245SR\255<T\200\342\236\r<\032x4\271\2434f\214\322g\336\214\322n\3074\326bi\224\322i3I\232PiE:\235J\0058S\205<\032x4\340psN\316\356\264\224\240f\214R\200{sN\036\364\204zP\033\024\026\246\346\220\265\000\323\324\323\211\246\267J\253!\346\241-M\335I\276\215\364\345\227\212S%p\331\244\315!4\240\342\2349\251\000\251\022\244\002\236\005H\016*E\004\367\251\000>\265\"\232\225MJ\246\244SR\003N\006\236\r?4f\215\324\002)3Fh\315!>\264\204\323M7\223M9\024f\226\224S\201\247\212p\024\340)\324\242\234)\302\235\232\0058\014\323\302\346\244\000\001H\334\216*\022\r\' \323\261\232i\024\332L\322\356\3058=\014\374UYZ\2533sM-M-I\276\220\311\203K\346W\036\335i\271\245\315-<T\212jd\000\324\300\nv\332]\264\365\251\024\323\305H\246\244V\251\025\252P\324\360\324\340\324\354\322\346\214\322n\244\315\031\2434\231\367\2439\242\233\315&\t\240\nZu(\024\341\322\236)\340\323\207ZZu--(\251\024T\200S\361M \322\021\212\215\215\013\232v3\326\243u\305Bi\271\243v)\013UyZ\253\026\246\026\244\335HZ\243g\305\n\371\025\313\223\223E\'J\003T\201\263N\rS\306\370\251\321\352]\364\006\247\n\221i\300\323\301\251\026\244\025 4\360i\340\323\263J\r)4\334\321\232L\321E(\243\036\224}h\3074c\326\224\016)\300R\342\226\234:R\212p\247\212u:\212P*t\\u\251\024sO\307\2454\203M*M\"\305\334\323\304`\016\224\205)\214\271\025VE\301\250I\346\232M4\265V\225\252\271j\214\265&\352\013Tl\324\212Nk\235u\301\244\006\226\232G4\345\034S\226\246SR\251\251T\324\200\323\301\251\001\342\224\032p5 5\"\265H\032\236\032\236\032\234\r(4\354\322\032i4QE8R\343\024\244n?-(\030\034\322\355\357J)\301sF)qK\212p\024\242\235N\002\236\005(\024\365^jQ\351R\016*D\030\0314\270\024\234f\220\221J\016)O\"\242u\300\252\317\212\251/\025\\\2651\232\240\220\346\2533TE\2517PZ\230Z\244OZ\304u\315W*E%\031\346\2274\340jE5*\232\221MJ\032\244\016)CS\203S\203T\212\325 jxjxjxjx4\340i\331\2434Rd\032>\224\242\224\032p\247t\373\264\240f\234\247\035E.=*D\311\030\244\307<\322\342\214S\200\245\3058\np\247\201N\002\244Q\3058\nxS\326\235\272\223u!jM\334\346\202\371\251#\177Z\212y;\n\250\354j\274\247+UKb\242f\250]\262\rUc\223Q\261\246\346\220\2657v[\025aH\305d\221Q\262\203Q\264~\225\021\0304\242\234\005<T\200\323\301\247\203O\rN\rO\rO\006\236\rH\r<\032\220\032x4\360i\300\323\301\245&\222\212)E(\247\212p4\341N\034\365\247\004=\251\300`\323\331r3M\242\234\0058\nu\002\244Z\221Fi\346\225E<\275FZ\224P\302\231\2323NV\3051\2335VV\305V\221\370\252\214\334\324.\325\013>\005BMFM4\232ajj\037\232\246\316*\211\031\246\021M<\036j)S\270\250\207\006\236)\300\323\201\247\003O\rN\rN\rO\rR+T\201\252@i\340\323\301\247\203R\003O\006\234\r;4f\226\212QN\035i\302\234)\302\234*@i\300R\266q\326\220R\322\212Zu(\251\026\245N\224\340(\346\2028\250\363\315.\361H\322SKSKR\027\244.1Tg\227\223U\336O\226\253\226\250\335\270\252\356\325\021zc50\2651\232\225\033\034\323\214\225X7\024\3074\314\322\365\025\t\034\320)E:\235J\r(4\340i\341\251\341\252Uj\2205<\032\2205<\032\220\032x5 4\354\321J)iE8R\216\264\361\326\234)\300\323\205<\032x\346\215\270\240\nZ)A\245\025*\324\240b\244QI\'\025\031~\324\322j6ni\244\323wTm.)\215!=*3)\031\315S\222M\315QH\374T%\351\217\'\025Y\236\241g\244-\221Q\231)\245\350\3631F\372\256\257\212\035\371\247+\002)H\300\372\324\r\301\240\002FiA\247\003N\024\341KE(4\360\325\"\265H\255R+T\200\324\200\324\212j@j@i\324\240\322\322\203J\r8S\2058S\251\300\323\201\247\203OS\212\220`\365\247l\006\243<\032(\240\032\225\rN\247\232\225M6S\232\256i\245\2526j\214\265F\317P\273\032h|S\036@{\325f\340\325y_\232\204\265E$\225]\244\250\313\324m\'\2753}&\374\232\031\251w\361\212\210\032\010\315 8\342\245\r\221H\311\270\344S\261\260u\241B\261\347\212xU\'\031\240\250\354h\002\212(\245\006\234\rH\246\244SR\253T\212\325*\265H\246\244SO\3158\034\321\232Pis\212p4\340i\300\323\301\247\003N\024\340i\340\324\213R\251\300\250\336\243\315\031\247\n\231*PjE4\217U\337\203Q\263b\242g\250\331\252\"\325\0335B\315Q3TM \025\003\2605\003\266*\273\266j\274\204\212\207\314$\323Y\351\273\351CsAl\232pj\211Z\244\315;\002\224qR)\024\222\214\257\035\2521N\025 \247\216:R\032\000\024\231\242\224\032x5\"\232\221MJ\246\244SR\251\251T\324\200\322\212u\024\240\322\203N\006\234)\324\341N\006\236\r<\032\221Z\244\335Lza\3068\244\024\3455:\036*@i\340\342\225\216EWaP\311\305Vf\346\232\315Q3Te\251\214j\007\252\316\rC\236\271\250]\372\324\005\361Lr\030Uv\030\250\230\323\013b\205|\323\225\275i\302Nh\024\240\346\236\265 \024\003@\2239\030\315\"\343w\315\300\247\034\003\3058\032x4\264\204SM&i\300\323\201\251\026\244Z\225jU\251\026\245SR)\247\322\322\321J)E<\032p4\271\247\003N\006\236\r<\032xj]\324\323I\364\247\n\224\034\npj\2205!|To \305V\221\352\2635F\317\305D\315Q3\324fJa|\323\030\344Ug\030\315Tr9\252\356\324\315\370\250\344q\332\243$\032\206F\3051_\013I\347`\322\371\340\016\265p\232\007Z\221H\247\223@\246\276\027\225<\323U\311\353R\016i\302\234\016)\300\321\232Bi(\024\365\251V\245QR-J\242\244Z\221jU\247\322\212\\\321Fi\324\341N\024\np4\271\245\rO\rO\rK\272\234\034R\202\r9y\247\223\232Pi\013\342\232d\250\231\252\027j\254[\232\215\232\230Z\240v#\245@\322Sw\344\3224\230\025RisU\267TNs\322\253\273\021Q\026\246\371\225\034\255\221\232\214\036)\214\334\324l\325\262M74\240\234\324\212r9\251\007\000\324\017\311\241x\251T\323\301\3158R\321\232)3J*E\251EH\265*\324\242\244\024\361R\003N\006\235N\242\2123N\006\236\r.h\315\024\271\245\rN\rN\335I\272\2245N\217\305H\264\342x\250\\\324d\323\031\252\031\033\212\256Z\230MD\307\024\322\303\034\325Iz\234TFP\243\336\253IrI\300\250\231\311\034\232\207q&\220\266\006*\0275\013TD\323$o\227\024\302p*&jh\311<V\306\354\323\t\251\243\373\274\323\324\323\363L#\212h\340\323\301\251\026\236)h\242\212QR\n\220T\213R\251\251EH*@i\302\234)\331\245\006\235E\024\242\234\r-\024f\2274f\2245.\3523OSS\257\"\236\246\225\215B\315L&\242f\250\035\252\026j\210\2654\265B\315P\263UIO\245W\315F\315@\340d\324,\331\311\250\213Td\323\r@\347\006\241g\315\0107\034S\300\330s\236\225\177\'4\365\251T\366\247\322\364\024v\244\305(\251\026\236)sFiiE8u\247\n\220T\212*U\251V\244\025 \247\001N\024\264\341J)\331\030\351IJ)h\242\2123K\2323@9\245\247\255N\215\305H\0174\3622*\026\025\021\025\013T/U\331\252&5\0335D\315P;TM\316j\244\200\255F\0334\331\037\013P\253dT.\374\361Q\226\244\335QHj#\3159\030(>\265\033=l\200\r4\360i\350y\251A\241\216\005,|\2574\273M \353O\006\244\007\024f\200iE:\234)\342\244Z\225jU\025*\212\221E<S\205(\247S\201\315(\245\243\351E\024Q\2323Fh\245\006\224\034S\251EJ\246\246\034\214\323\201\241\205B\343\025]\352\007<UF=j&j\214\265D\355U\334\323\013T/\363US\36256^V\241\034-B\375j3Gj\216R\002\324!\276Z\214\2754\265n\253\323\261\232p\030\251\001\247\021\305:1\201O\307\345L<\032)\331\245\006\224S\251E<T\213R\250\251TT\252*@*@*AKE(\247\np4\354\203\332\222\214\372\322\023I\2323Fh\315\024\271\247\003N\024\361O\034T\250\335\251\343\031\247\023U\345`*\243\275Ww\252\356j\0064\306\346\242cPHs\322\241-\353M&\253\3102i$\000\'\275@zsQ=ELv\300\252\222K\232a~*2\324\205\253l5L\215S)\315H\026\236\005=W\025 \034SY8\250\250\247\nu:\234)\342\244QS(\251\224T\252*@*E\024\360)\333h\305&)\302\224\nZ)\246\212L\014u\244\242\214\321\232Pi\300\323\203S\203S\301\251\024\323\303\363C>\005S\226L\232\256\355P;T,\325ZG\346\243\3631Q\273\347\2450\236*\273\2674\200\346\220\214\232c&[\223\300\250g\340qU\230\361P\261\305C/CU\030b\243&\233\232ij\334\006\244SV#j\264\234\212x\342\244Z\220\nR\274Ug\034\361M\245\025 \024\243\255<S\300\251\026\246AS\255L\242\244\002\236\005H\242\237\212LQ\266\227m\030\243\024b\223m\030\246\220;SN1M\242\214\321\232]\324\340\324\340\324\360\365\"\267\024\026\346\221\334m\252r?5\0135@\355P\263\324\016rj\007\342\242\3630y\241\233\212\205\201\355LRKb\207s\273\002\230M5\210\306\rVu\306qP7Z\205\370\252\322T\004\323\t\246\223[\240\324\212j\304f\255!\251\001\251Tb\246ZRj\263\016i1@\024\360)\300S\200\251\000\251\024T\312*e\0252\324\242\244\025\"\212~(\305&\3321K\212LQ\2121AZiZa\\\347\024\302)\244RQF}\350\335N\rN\004\324\212\330\034\323\032L\232Fo\226\252\273TL\334Uy\036\253\264\225\023I\3151\233\212\254\347\232M\365 \341y\250\207\031\367\246\232k\032\201\330\223M \221PH*\263\236*\263\234\325v<\323\t\246\223[\252je5<ue\017J\235je\247\203J\307\212\212\214R\201J\0058\nx\025\"\255H\242\245QS-L\242\245\002\244\002\236\253O\002\235\2121HE!\036\224b\226\214Q\212\n\324l\246\242a\212a\024\332C\326\233\2323N\006\246Zk\266*\271~i\314\374T\016\325\004\217U]\352\274\215U\313\234\322o4\3269\246/Z\224\266\0056\243s\212\215\232\243=i\340qPH9\252N9\301\252\257P7Z\210\232a5\274\225:\212\236:\262\274b\254!\315J)CzP\\\343\030\244\034\322\201OQN\333N\002\236\005<\n\221EJ\242\245QS(\251\224S\300\251\000\247\201N\305\030\244+M\305\030\244\245\006\226\202)\244TN9\310\250\230S\010\246\232i\024\224\245\200\245\022\340To&\352\217<\3223\361U\336LUg\222\240y*\006z\205\233&\233\272\215\331\247(\346\234\313\315\025\024\234\232\204\365\245\305/j\205\272\223T\346\0309\252\255\311\252\362pj\003Q\265t\010*\302\234T\250j\312\363V\022\236\030\032\025\261\320\323\363\221\3159O\030\247v\247(\247\201J\005H\005<\n\221EH\242\245Z\231jU\251@\247\201R\001K\212)\r%&(\305\030\243\024\021HEF\313Q\262\324ei\204S\010\246\261\305G\313R1\300\342\233\232kTL\330\252\262\2775Y\332\240f\250\331\261Q\026\346\233\236i\353SD2j}\234\234\324.1P=DFi@8\305(\004\214\nc\256\336\265NnsU\010\353U$\250MF\325\321\"\032\231S5*\200*\302\234\nw\231\330S\303\032r\232\225NjAO\024\361O\024\360*@)\341i\340T\212*E\025*\212\231jU\025 \024\3608\245\"\223\024Q\212m\024QK\232Ji\024\302\276\225\031J\214\2550\255DS&\221\206\321Q\221\232\002b\230\342\252\313T\344\252\256j\022\365\013\276M34\n\231*\314?xU\2228&\253\311\336\25352\227\034qD|\212\216j\243%@zU\031\206\r@M1\253\250\351J[\002\205j\231Z\245S\353S)\030\251\027\232\221V\244U\251\025\rJ\261\232\221b&\236#5 B)\352)\340T\200\n\221EJ\253R*\324\200\021\326\236\265 \024QHE%!\024\224QKIE4\212i\024\302\275\351\205i\206<sP\310\274\323U;\320\303\002\240\220\3259\0175NS\212\246\3475]\215DM%*\324\351V\242\030\"\255\0221Ue\252\315M\357OQH\027nj\031\252\224\225]\370\252\322\256j\243\014\032\214\327LOzhl\232x52\232\225MJ\2652\232\235MJ\246\246Z\225EL\2652\212xZxZ\\{P\024}*@\247\352)\340\343\255L\2305(\024\270\245\315\031\245\242\220\212Ji\030\242\212L\321\2323IHi1\232k\014Tl{T/M\350)\215Ue5Q\352\224\334\232\252\374T\014)\204S)\351S\'QV\242\346\245|\214\021P?5\023-DF\rH\2074\3622*\244\243\004\212\251(\346\252\311P\236j\244\313\203U\315tE\363J\265*\212\231jU\034\324\312*eZ\231T\324\310\246\247U5:!\251UjP)\342\236>\224\341N\306h\000\216\224\340\304u\251\025\324\366\305J\030v4\241\251w\212p#\265-&)(\244\'4\224\021\232m\024\231\245\315&x\244\007\0249/\315DEDW&\220\214TRUIj\243\203T\344\035j\263\212\205\2051\2050\255*\212\235\005X\214\342\236_\327\221Q1\007\245\'Z\214\216x\247\001\265M&x\252\322\034\346\251\310j\273\212\200\214T2.\341U$\\V\332\324\253S(\251\224T\313S\245N\265:T\350*t\025:\212x\031\247\201N\002\236)qJ)\334\322\322\201N\024\365\315H\006i\333\005.\334t\244\311\024\231\245\315\024\322)\264QHi3I\232:\322\206\300#\035j3M\353LqU\336\253H3U\334dUI\022\252\310\265\\\212\217\0314\2453@J\231F\005<u\247\3435\013\251C\354h\246\363\221J[\203M\317\006\253Jj\253\014\324.*\"\264\306Z\253*f\264\326\245Z\231jU5:\324\351V\022\247AS\240\253\010*e\247\212x\247\016\224\372p\024\340)\300R\205\247\205\245\013O\013O\002\235\212Z\nR\025\244\331\355M \212NM!\024\332)\247\336\232i@&\244T\241\222\233\345\226\031\364\2462\355\250\330T\016\271\250\031*\273\307U\244N*\244\211U\236:\214G\315?e\033iqKO\007\212d\274\212\214PG\024\303L\317\006\240~j&\025\003\212\205\2054\212\201\305\\SR\251\251\222\246J\235*t\253\tV\022\247J\260\225*\324\213N\024\360)\340S\300\247\201N\305(\024\340)\340S\261O\002\234\026\245H\367\032\227\311\002\232\321q\3050\307\216\325\033% \212\220\307\352*6OJn\332c\n`\0252-HN\0054\363H\006\r1\223\'\232\215\223\025\031AQ2\n\255\"\325YW\345\252\016*\026\031\244)\307\024\230\246\342\232x\240\034\322\366\246\236\234\322\001JG\025\021\025\031\351L+Q:b\253\270\252\357L\353Q\270\251\301\251\226\246SS-N\225a*\302U\204\253\tV\026\244Z\221i\340S\305H*@)\352)\370\243\024\340)\300S\300\247\252\324\251\036MXU\300\247\342\232E4\212\214\2504m\024\233i\245\001\246\030\207j\201\3415\037\226\300\323\324\021\326\220\344\232r\255;\024\240T.\274\324\0141P\260\252\356\265^Q\305Q\2219\250\n\321\212iZ\215\306*#J\253KH\302\231F\354\212k\n\214\n1\326\242q\315V\225j\233\216i\240b\243z\225MJ\246\247J\231*\302T\351VR\254%YJ\231jU\251\001\251\026\236\242\236:T\252*@;\323\326\227\024\340)\301jEBzU\210\340<f\247\010\007\002\234\022\224\2454\212a\024\314R\n\r7\255%\004\003M()\245)\246:M\270\246\232BqL$w\353Q8\025\013\255V\222\2538\315U\221sP4t\2051L+Q8\3153e.\321MaM9\"\230E ZR\271\024\2018\244+\324T2.*\254\235\rTu\346\242#\025\023sOJ\231jt5:T\350*\312\n\260\234U\2048\251\325\205J\257R\253T\200\324\212jE5*\232\221MJ\265 \366\024\242\236\242\245U\315[\202<\014\232\225\260\243\212\022\246\002\224\364\250\030\363Q\223LcM\3154\232L\323wS\201\342\214\321\232ajN\325\031\036\224\326\250\315F\325\013\232\257%VcQ5Bi\244f\230\313Q\224\246\221L\"\230E0\214\322\021H\005=W=\2516\342\232G4\307\213p\342\251I\0363\232\253 \252\354)\205i\026\245SS\245X@j\302\n\262\203\0252\324\352jU5*\232\225MJ\246\245SR\255J\265\"\324\252}j@\376\224\345\253\021\256j\312 \0258\245a\220)@\305.\354P[\212\211\215Fi\246\232i\246\232M0\365\247\003JM74\003\203\315)!\272P1QH3\322\240\"\230\302\241aP8\250\031j\007J\205\201\024\332\017Ja\031\250\330TdTl9\240-!\024\3209\251T`R\021L+\221M?v\252\3123\232\246\351\315B\321\346\242d\250\026\246A\232\261\032\325\244\025:T\313S-J\246\245SR\251\251\226\245Z\231jU\251TT\252)\364\340*E\025n\016\225d\032z\232\225H&\236j2)\246\2434\001M4\302)\270\342\230E4\212i\310\240\236)\205\216i\300\322\346\214\323\032\242jcT-\315B\3035\023\n\211\226\241d\250\312R\025\246\025\2462\346\243\"\231\262\224\255F\303\024*\323\310\307Zn3F*\'\\\016*\273\256E@\361\324-\035D\351Y\351VPU\210\352\302\324\252jUj\221Z\246SR\251\251\224\324\313S\240\251\324T\312\2652\212x\251\000\247\205\251\000\253p\217\226\246\013O\002\234\016\r.\352\\\323MFi\r\'ji\246PE0\323M0\323\010\244\316)CS\263\3055\215Fj&\250\332\243j\215\206j\"*2)\245)\245)\214\225\023%0\245\0331Me\250\331h\013M#\'\232\\\036\324\214\204\014\323\010\312\234\325r1Q\260\317Z\211\226\240u\315e\240\251\320U\204\310\251\224\324\213R\255L\246\245SS\245N\225:\n\235\005XAS\250\251Ui\341jEZ\220\n\225S5f5\300\346\246\006\2349\245a\3050\032\\\322\023L4\207\245(\2468\364\250\311\246\026\246\226\2439\246\236)\206\232E\002\234\r\006\230\334T/Q\036\264\323Q\232k\014\324eh\tF\332c\257\245B\313L+M+\212a\025\031\034\323H\246\323\227\255\0142)\254>^\225Y\205D\302\241qU\333\203Y\310\2652\216\225*\324\253R-L\265*\324\310*\302\n\260\202\254 \253\010\265a\026\254\"\325\210\342-\322\245\3733c4\2331OU\251Pb\254\257LR\201\315H\242\211\016\005C\272\227u4\232J)E-0\250\250\331*2\224\241i\214)\230\240\212LR\205\240\212\215\252&\025\031\024\302)\204SqM+J\005!\024\3223P\262\323\n\342\232EF\313Q\221L\"\232V\2000i\3148\342\230\303+U\330Tl*\t\005Wu\254\325\034T\253R-L\265*\212\231EL\202\247AV\021j\302-YE\253(\265a\026\254*\325\210\320\366\253pr\n\277J\212E\033\270\024\005\247\201R\3069\251v\363OQQK\311\250H9\245\353IFh\315\000\323\201\243\024\230\315!Za\025\033-7m&(\305=S\"\221\226\242e\250\331j&Z\211\205F\302\233K\214\212\000\241\205FE4\212\215\226\243\"\232ED\313L+M\"\220\214P\t9\305\005p\rVa\315F\303\025\013\014\324N\225\222\005=EJ\265*\212\231EL\202\254\"\325\204Z\263\032\325\224J\262\211V\021jtZ\2361\310\315\\\014\252\240/Z\025\210<Q\214\320F)sO\214\363V\200\340\032x\030\025\013/4\322\242\230V\232E!\244\242\200i\342\236\006h\333\315!L\324n\225\036\312iZLsR\240\244aP\260\250\210\250\310\250\312S\031*\"\224\230\305\000\320Ni\244SJ\323\010\250\330S\010\250\310\2462\323\010\244\333\232ENi_\345\004Ub*\026\246c\232\032<\212\300\006\244Q\232\225EL\202\247AS\240\253(*\302\n\263\032\325\250\326\254\242\324\352*U\0252T\242\244\024\374\322\023\232J\2325\357V\024\361O\3154\256i\245i\204S\010\246R\021\2121\305 \034\324\2128\247S\363\221F)\031A\250\331*2\224\302\264\345\024\244TL*\026\024\322(\331\232C\021#\245E$[G5Y\206)\270\315\030\244#4b\243aQ\232a\024\322)\2148\246\025\246\221GN\225\013\222MD\302\243+M\013\3159\206\005sj*U\0252\212\231EL\202\247AVPU\230\305Z\214U\250\326\254\242\324\312\264\361R\251\247\203O\006\235\272\2234\344<\325\264\344\014T\341x\245\333N\333Me\250\210\2460\250\332\243&\223u\000\346\236\r<\032}8R\342\223m#%DR\200\230\246\260\250\331j&\024\300\265,i\223S\004\030\306*\033\230\376Z\314t\346\243\306(\245\333I\214Tl*6Zf)\244S\010\246\221M)\221Q\343\002\243aQ\260\250\312\321\216i\262g\025\316\250\251Uje\0252\212\231V\247AVPU\230\305[\214U\250\305YQR\n3O\006\236\r<S\351qOU\253p\203\212\264\2434\340\224\3420*6\025\021\025\023\n\214\212c%0\255\030\305(4\365\251\001\247\346\234\r8\n\220 \"\243h\361L)Q2\324n1P\232@*P=)\335\2529\016\340A\2522\2475]\3053\024\365\031\244e\250\210\250\310\246\221I\267\"\230\313L\305\0140*\006\246\021Q\260\246\204\'\240\245\331\216\265\004\247\002\260UjUZ\225V\246U\251\325jdZ\262\213VcZ\265\030\253IS)\247R\212z\212\221EH\005<\n\221V\246T\251\223\345\251C`\325\205pE\014)\245i\214\265\013-D\302\233\212B\264\322\264\3221FiC\322\371\225*6je5 jBsHF*\'\"\240a\232\211\226\243\350jT4\346\2467J\255(\252\222\n\212\244Z\030TL\265\031\024\303I\315!\024\322\264\306\034T\004S\010\246\225\245\215y\311\244\223\212\245)\306k\031V\246U\251UjeZ\231\026\247E\253\010\265e\026\254\245N\265*\323\300\247\205\247\201R-H\242\244QS\242\324\341qMn\r9[5*f\247\316E(\244a\221Q2f\241d\3057m\033i\245j\027Z\214\323y\240\023R+\021R\254\206\245\022R\031h2f\230Ni\206\230\302\230V\234\274S\310\342\242j\201\352\273\214\324%piV\202i\206\230\302\243\"\223\024b\232\303\212\215\272T$R\005\243\313\356hn\007\025VJ\25175\230\253R\252\324\312\2652-N\213S\"\324\350*\302\n\235EL\202\247QR\205\251\002\323\302S\202\323\300\251TT\351\305L9\244d&\205\201\210\310\025*#\016\2650\024\243\203A\2445\023.i6Q\345\323\031*\026CP\262Rm\246\221\3158-/AFx\244\0074\271\245\245\353\326\202\264\322\264\233i\370\342\241a\315B\300TEj\007^j3M&\233\232\t\246\342\232E&)\2568\250H\246\021@\024\343\322\243qU\234UIEg\252\324\252\2652\255N\253S*\324\352\2252%N\253S\242\324\312\265:-L\253R\252\323\302\322\343\332\200*U\025*\212\261\027$U\305\010\247\347\0314\327\230.Dc\203O\211\303\2140\0242m84\321\212\221c\315+[\236\324\337 \322\371\036\324\206,Tm\035@\311P\264u\013Fi\233M\024\322i\t\244\315(jx4\341J)i\244\ni<TOQ0\250\332\243e\252\3561Q\323H\240f\214\342\221\2104\001L\177AQc4\322\224m\305\014*&\025\003\214\325i\022\263\324f\246U\251\225*tJ\235\022\247T\251\325*eJ\231\022\254\"T\312\225(JxZp\024\355\264\241jEZ\224-H\274T\200\223R\004\006\244H\361Sc\214\036}\352&C\236*xP\236\247\025q\023\324\212\220\306;\n\215\242\250Z:\211\343\250Z:\211\243\250\031=\252&\2175\023\307\212\205\226\230E&)@\247\250\251\002\323\200\3055\251\271\244\250\332\243aQ\221\3155\207\025\004\211\232\204\256)\207\2321M\"\224-+\014\n\204\212n\332wjk\nc\n\211\206j&\025^E\254\364J\235\026\247D\251\321*tZ\235\022\247D\251\321*uJ\235\022\247T\251\002g\245)\214\257ZP\264\270\245\002\245QR*\324\201i\301i\342\245V\305<\267\245\033\260*H\334\032\2345H\256A\342\247\014\030r)\214\202\241h\352&J\205\343\252\354\224\302\225\023\307U\335*\022\264\335\264\340\265\"\2558\322c4\322\270\250\317\024\335\324\204\346\232V\242e\3050\323\030f\242t\364\250J\320E7\024\360\000\246\260\334*=\264\355\235\351\254)\245j&\025\023\212\205\215B\346\251*T\350\265a\022\247T\251\321*\302%N\211V\021*tJ\231R\245\tR \301\342\234\371f\344Rb\223m(Z\225EL\213\232\235c\315N\220\257\361\032kE\203\305FW\024\240\361Mf8\250\304\205\032\255G85:\316\007z\221nTT\242ezL\212i\025\033.j&J\215\243\025\013GU\244J\252\353\212\217\024\341\232\224\003F(4\306\250\332\243\"\201\326\244\n\rG$uY\306*2\336\264\322\300\212\205\210\246f\216\364\356\270\024\366A\267\212lj6\234\323\037\212\214\323I\246\032\205\305V\220Uv8\250\343Z\260\211V\021*\302%N\211V\021*\302GS\242T\312\2252\245J\022\236\251\203\234T\205\001\034\365\246,T\246:M\264\340\2652\014T\352jU\004\364\247b\232S4y&\230\321b\243h\201\246\204\333FqNQ\223\315Y\215\200\351S\357\243}\005\263M&\230\303=\252\026\025\004\213\236\325\t\267\334y\240Z\001A\267\307Ji\217\024\335\224\205*2\264\306J\205\2050\361M/\216E\006\347\214\032\247<\343\265Q{\203\353Q\371\344\3654\276a=\352T9\251vq@\0304\374\346\225z\232c\255B\303\025\031\250\311\250\335\252\274\225U\372\323\343Z\262\213\232\260\211V\021*\312%XD\253\010\2252%N\251R\252T\312\224\361\035/\227JW\035)\273sF\312P\230\247\001\212\220T\310\330\251G\315\322\230\310A\245\010\330\344\322\265\273c \346\243\020\261\353R\01029\024\306\266\3054B\001\346\244X\300\350jQ\037\024\306LS1\351F\342(\363=\251\013\2554\225\365\244\033OJB\0054\201Q\262f\231\262\232\313Q\354\315C7\313\300\252\304\346\243~*\244\262b\251\311>*\263;9\342\230\310\330\252\316\305\r\t6MZ\216J\271\034\231\024\245\250\006\244\003\220hqP0\250\230T\rQ1\250\034\325g\025b1V\343Z\262\211VQ*\312%XD\253\010\225:%L\251S*T\213\036j@\224\375\224\236^iD4\030\351\276]\'\227J#5\"\241\025*pjl\202*\t\t\3155e`q\232\23599\315XQK\260\032cB\rG\345c\245(CC\naQ\351Q\262S\n\232aZaZiZL\021J3\336\227\024\230\246\262g\2655W\373\302\251\\\217\234\342\252\234\324o\310\254\371\301\346\251\030\231\317\025n\336\323\035EZ6c\035+.\356\307\004\220+5\241dj\221\033\025f)*\312\266i\300\346\254 \312\212V\217#5\013&*)\026\252H*\273T\rP\275[\214U\270\305[\215j\324kV\221*\302%XD\251\321*eJ\231R\244\tR\004\247\204\247\010\363N1\2208\024\303\037\255\001\005#(\355H\006(\357J)\313\326\207L\324b0O&\254\306\212\243\203\223R\201N4\337\251\240\342\231\232Ni\254*\")3\232FL\364\250\331qL\"\233\212\220 \3051\227\024\231\2434\202\241\232\000\340\234sY\322&\302EU\220\342\241\333\270T\220\300\255\332\256\307\000\003\201S\030A\035*\254\366\201\201\342\261/l\366\202@\252?g;rE5\020\206\255(-L\213\220y\241\340h\31755\271\347\006\247t#\351P2\324\022-T\221j\254\202\253?Z\201\315_\214U\270\326\255\306*\344B\255F=*\324kV\021j\302%N\251R\252T\201)\352\231\251U)\341y\241\200\355Le\246\021\212n)6f\202\270\246\322n\"\233\274\232o9\251\341<\363V\326\224\212L\n\215\200=)\002\232\017\024\322\302\230j2)\271\"\232Nz\323)\010\346\246P1Mq\223Q\342\223m.\312\n\341O\025\233p\233\311\300\252o\006:\324\"\"3V \214\203W\221x\247\221Q\262\346\252Ml$\353U\215\222\200F*\224\226![\345\025b\010\031\007\024\351>a\206\034\325`\233_\"\254\261$sP=@\342\253H\265NQU$\030\315U\220\326\254kV\343\025j1V\343\025j1V\243\253Q\212\262\202\254\"\346\246U\251U*UJ\220 \240\2554\2554\2550\2550\256\r7\034\321\203M\"\230V\223e(\2175,i\203V@\247m\3155\206*<sA\351\311\250\330\366\024\334f\215\206\215\206\230\313P\225\246\221I\212\221\006N)XsM\333N\tK\267\035i\030\006\025VH\324sT\245\000\232j\302\032\245\020\340\323\300\305.(\3050\256j3\0350\302\t\311\024\024\000t\252\362\301\225$U=\234\363O\"\242a\305WqP8\252\262\255P\230U9\005l\306*\324b\255F*\334b\255F*\312\n\264\225e*\302\n\260\202\247QR\005\247\001A\024\204Sv\323v\323YsL)I\262\220\245FiUsR\025\013J\204T\200\212]\300R3\203Q4\212:TM \365\246\207\006\234$\002\227\315\364\244\336M!\031\246\225\024\323\036j2\244R\253m4\343\311\315(\245\310\025\013K\223@\220c\025^c\232\250\312I\253\020\307\305LS\212a\\\016)\204RSM\024\322)\244TdsPK\010\373\302\241)\305@\353P2\324.\265ZE\310\252\023%R\221+^1V\243\025n1V\243\025f1V\220U\204\253\021\212\264\202\254\240\251\324T\240R\342\214R\021I\266\220\2554\214Q\2674\322\224\306\035\251\236^i\215\307J\211\331\2155d\"\236\327\001G\0315\013]\023\320SE\303w\244iKv\241\010\357R\0021F\352PsOZ\220\014\323YiB\346\225\320\001\223U\037\031\371i\013\343\251\246\231\207\255Ff\31579\247\n\n\347\265\036Ni\352\233zS\261H@\250\230\n\215\2522Nx\244\335FsHi\206\232j7@j\264\221\343\245Wt\250\035}j\264\211T\345\216\251\274y\257\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\004\255IDATx^\355\335\321\226\2338\014\000\320\036\372\377\237\2749{\232m\272\035H\006BlcI\367>2\t\030ld\311$\231\037?xfYo\000\000\000`\210\217\353\261\345\343=\300\264n\353\rl\010\001\000\300\347E\005\000\000\000\031<\312Ck\353\000\000\000\323\363\204\017\020\t\3302&h\303H\342\210\233g\t\000\000\300d\324\263W\370\2738\324\003\000\000\000@0\341\2273\302\237\000\000\000\000\260\021\355\307\342\2075\3277\030\000\000\200\234\326eU\344\352g}.\ru\334\365\305\362\236\031\000\000\000\007(\013y)\362\"\021p\206)\201\277\231\005\240\004\241\237\223\226\237\353-\320U\343h\325xw\034\"\273\004\000\000`\237\352\221.\014,\200\227\254\227\003\000@1\212\000\000\000\n\363\330\020\000\200\272,\016\003p\221\321\0132O\247\274\345K;n\267\321\215\002.\36642\000\000P\213J\020\000\306\2122\367Fi\'=X6\204h\304l\310\313\375\235\314\3414\353\360\013\001`\2079\005\340MY\002\347\222\345D  \367\037\000\000@2\236\333G3\2602\367\273\014\274\305x\201\310\334\301\000\014a\302Ii`\225JSz\016\000\200\246$\230\305\031\000\000\025\025X\360Mv\212M\347\353\246;\003\372s\323\206\240\233\000\n\032Tt\014:\014\2632\000\000\000\000\240\252\343\217\036\254\037\000\000@\025\235\263\377\316\273\347s\307+\305b:]\230N\273\2459=\005\000@8\007+p\271nZ;\377;\360\373\277\002\344\'\016\262og2\005 \253I\342\377\301\262~>a\033\036\322m\226\361\n\220B\2429\314\364\000@\017\346\027\222J\224\005\002\000\345\311l\000\302\020\262y\305\032\034\000\000\364%\347&\224\027\013\010}\307\361\355\353Q_\264\001\250\304\267l+\373\325\371\006\000\000\344\243\324\203\000$\342\000\360\006k\230\000\033\277\213\177\0012\000\235\004\225\211\000\000\000\344\341\203\010\331\254z\364Q\276\334\326\3376\340\177\313\372\252Q\315\310\001`I\001\000\270\222\\\204\211\030\216\343\214,y\016\233\262Q\326N\200\234\3046\000\210\310\014\376\233\013\221\307\234KAs\266\n\262\021\313\001\000\230\205\334t\270 \227<H3\001\000\000\346\263}\340\252\304\002\250\314\017\214\027\267\374\\o\001\000\312\220\010\002\000\000q\251h\342+\327\207\345Nx\207\353\001@j&:\200Wr\177b9\367\331\3255\240_\007\034\3423\235s\233\315\3567\033\022\230\276\223G\312\330\301\234g<\000\000$%\321#\261k\213\374k\217~R\310FS\324}\3762\211\001\300\365\244\220\314l|\2768\376\210@\024\342\003\000\000\314\302z\026\000\000\000@\024Vr\000\000\000\000\000\000\000\340n\211\366]\345h\355\005\000>g\376\207\366\334WD\341s\337\245\351\376\016\"\305\177\003\240\270\267\007@\244\321\3153\341\026\252\001\242y{n\005\030\357v}\254\272\276\005\000\000\000\000\000@X>\376\002P\234\'\316\347\334\\:\000h\313\314\n\000\205\230\370\001\000\200\231\004\371\350\214R\252\270\t\276\316\016\000\000\000\300\033,\347\0009\210f$\026\344!!\000\320\310\005\037\274\270\340\220\034!\017\344\356\327\r\372\317z#\005\374\t\315\213E\017(h\221\007@I\2339\177\263!\006\021\254\021\027\222\347\202F\006\336%\004\024g\000\000\000\360\214<\021\000\200\324<\002I*Q\307\216/\312\336:\342[/&\212D7\020\224&DCY\207\277\010\262\010\024I\035\034\000\000u\325\230\001\217\235\345\261W]\250m\276b\216\004&\3264\336\001\334\265M\245\276!\313\232\323\260\001\300\\\222\335\220\311N\347\205\0067\353%\027\252\347\017\0076\270&\000\325\010\235\000\274\3201q\'\262O\006\306\355\243w\037\361\310ld8g=z\250wO}\357K\377\035\353\314c\257b\353\332\256\006h\313l\000@\006r\364}\3679\337\205:)\370\205\013\336|`\000q\002\000\370\236\222\362S\255\027\342?\351\214\263\357=\373>\272h:\244\364mu{\303\311\010\231\220N\001\372\370oJ\330\233\030\200\224\"\244\027O\303S\204\206\267\223\366l\307\235\330\270#\r\326\363\267\007\000\000\000\370c\306\352\353\374\217^.\347\337\032\315\214\035\007\227+\023\001\000\000\330\220\013\326\266\314\264 0OKz\362<\037\000\340b\023\344c\0234\001\362\333/\276j\024\241;\016\\\204\307K\376\005s\343\23463\025f\327\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-3.textpb b/core/res/geoid_height_map_assets/tile-3.textpb
new file mode 100644
index 0000000..486adf4
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-3.textpb
@@ -0,0 +1,3 @@
+tile_key: "3"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\364/\245.\352pz\225d\247\356\3151\316j65\0314\302i\245\251\244\323sFh\315&h\315\004\323I\246\223M\335HZ\232M4\232ija4\302i\204\324d\324d\323\t\250\311\246\023Q\223Q\263TL\325\0235D\315Q3T,\325\0135D\315Q\263T,\325\0235D\315Q\263Te\251\205\251\244\323I\244\315&h\315\031\242\2274\240\323\251\300\323\201\247\003R\242\223Vb\212\264!\213\025\353E\251\245\250\017O\017R\007\247\026\030\037Zk\032\214\232\215\215FM74\233\2513K\232L\321\2323M&\232Z\230Z\233\272\220\2654\232ijijaj\215\215FZ\230Z\243&\243-L-Q\263TL\325\0235D\315Q3T,\325\0235B\315Q3TL\325\0235D\315Q\263TE\251\205\251\204\323I\244\315&i3Fh\315.isJ\r;4\340i\302\245E\315[\215*\364H\000\253I\355^\231\276\223}\033\351\341\351\341\351\301\363O\334)\215Q1\250\211\244\315&h\315\031\2434f\220\232ijajaji4\335\324\026\246\026\246\026\246\026\246\026\250\331\2522\324\302\325\0335F\315Q\263TL\325\0235D\315P\263TL\325\0135D\315P\263TL\325\0335D\315Q\263Td\323\t\246\223M&\2234\204\322f\214\322\356\2434\240\322\203O\024\361R\242\022j\334q\364\253\221\307\264d\325\204\253q-z\000z7\322\356\245\017N\017OW\251\003\346\235\2735\033TML\315\031\2434f\214\323sHZ\230M0\2654\2650\265!j7S\031\2522\325\031jaj\214\265F\315L/Q\227\250\331\252&z\215\236\242g\250Y\352&j\211\232\241f\250Y\252&j\211\232\243f\250\213S\013S\t\246\223M&\232M7u!jM\324\233\251A4\360\rH\020\323\304u\"\305S\254F\254\305\016:\325\250\323\025co\002\245\215*\344K\212\355\003\346\215\324o\305(\222\234\036\234\036\244Y*P\371\240\267\025\0214\302i7R\346\214\322\023M&\232Z\230Z\230Z\230Z\232Z\223u\000\323\034\324L\325\0335FZ\243/Lf\250\231\2526z\215\236\242g\250\331\252&z\211\236\242g\250Y\252&j\205\232\242f\250\231\2526j\214\2654\2650\2650\2754\2653u\031\247\005&\234#8\351J#5\"\307\355R\244C\275N\261\217J\231b\036\225*\305R\004\002\244QSF9\253\210\231\253\021\307V\022:\350\326Jxz\013R\007\247\007\247o\251\025\352U\222\244\017\221\3150\236)\204\323wR\206\245\335HZ\232Z\230M0\2650\232ajijM\324\233\261HZ\240s\212\211\232\243-Q\263Te\2526j\211\232\243g\250\231\3526z\211\236\242g\250\231\352\026z\211\232\242f\250\331\252&j\214\265F_\024\302\365\031~i\013f\232h\000\232\235\"\365\253\t\037\240\251<\272M\230\355O\013OU\315J\253\212\235G\025(\034RT\252*\304K\315^\215j\312%YE\2550\324\360\324\355\324\205\2517\323\303\324\212\365 zx\222\234\030\021McL-\315\033\250\335HZ\220\2650\2654\2650\2650\2654\2654\265!jaj\215\232\240f\305F\315Q\226\250\331\252&z\215\232\242f\250\331\252&j\215\232\242f\250Y\252&j\211\232\243f\250\231\2526lT,\377\000\205F\\w\246\347=)*EJR\242\204Q\232\262\007\313V\020p)\370\243m(Jz\256\rJ\027=*@\265 \024\273y\251\221j\314C\025v<U\250\326\254*\325\260\330\245\337K\276\221\244\246\357\247\253\324\241\351\341\351\302Jz\311N\337\236\264\306<\323wQ\272\215\324\322\324\322\324\322\324\302\324\322\324\302\324\322\324\322\324\302\365\0335B\315Q3\324l\325\031j\211\232\243f\250\231\2526z\211\236\242g\250\231\252&j\211\232\243-Q\263TL\330\250X\236\246\242f\246S\200\245\035j`@\024\335\324/Z\234\036\225j#\225\251@\247\201O\013N\333O\214`\324\333)\300S\202\232\231\026\254\"\036\325b4j\271\0225[D\251G4\326\342\243/Az@\364\340\365\"\311O\017K\346S\326J\225d\343\024\026\244&\233\272\215\324\205\251\013S\013SKS\013S\013SKS\013Tl\365\033=D\317Q3\324e\3522\324\306j\205\232\242f\250\231\3526z\211\232\242f\250\231\2526j\214\2651\232\241rs\234\324li\206\223\034R\321N\240T\2129\253\n\234f\255B\277(\251\302\323\302\323\202\324\252\225(\216\236\026\244T\251\026*\263\025\271n\325r8\002\216je\n;T\311\364\253Q\256j0})\254j\00684\335\324n\245\335N\rR\007\247n\245\337OY)\341\363\212v\352Bi7Rn\246\226\246\226\246\026\246\226\246\026\246\226\250\331\352&z\211\236\243g\250\331\352&z\214\276)\245\352\'j\205\232\242f\250\231\2526j\215\232\230A=x\250\233m0\342\230Xv\024\302F*&\344\323H\243\030\242\212QR(\251Q7\036*\340N\000\253\010\230\002\246U\251\002\324\212\225*\245J\022\236#\251\243\2135j8=j\342\"\250\342\2022i\311\035Y\215*\324Q\346\250\253qC6*\t\rE\272\200\324\340iA\247\206\247\006\243u(|S\325\352@\371\247o\365\246\356\315!8\246\226\246\026\246\226\246\026\246\026\246\027\250\331\352&z\205\236\243g\250\231\3526z\215\236\233\277\024\326z\205\232\241f\367\250\213{\323I\035\3114\302\303\265F\315Q\026\246\223Q\261\246\3474\206\220\2121I\2121N\002\245E\315[\202*\274\261`\n\225R\244\013\212z\256juJ\220-J\251R\252U\204\001G\275N\274\324\240b\225FMX\2162{U\350\255\275j\354p\001\332\271\250\344\342\234NEA#qP\226\240=<58\032pjvh\335F\352]\330\247\253\323\374\312R\340\323Kb\232Z\232Z\230Z\230Z\230^\242g\250\231\352&z\211\236\2432TL\365\031zc=3}1\244\250Z^j6\222\241g\246\357\365\246\226\246\026\246\026\240\364\250\311\244\315-\006\233E\030\247\250\253Q%^\211qV\224t\251\200\245\013\223\305N\211\212\231S5 \216\244T\253\010\234T\211\036MXX\352d\2075a QV\243\214/AV\343Bj\322%p\361\276j\300<T,x5Y\233\006\2205<=H\032\227u;u\033\251CR\356\245\335K\272\215\324y\224\205\275)\205\251\245\3526z\215\236\241g\250\231\352&z\210\275F\317Q\227\250\331\3522\364\322\331\250\331\261P\227\250\231\371\246\026\246\227\246\027\246\227\244\335Jd\342\233\232)\331\242\214R\221@\031\251\243J\273\nU\245\\T\350*`8\247\242\022j\334QU\205JxJ\221c\251\3250*x\343\346\254\254u:ES\244&\255G\020\035j\302(\035*\312-y\324G\275YS\221Ls\214\325)\033\006\232\032\234\032\236\257R\256[\245\004\225\353F\374\322\206\245\rK\272\227u\033\251\013\323K\323K\323\031\352&z\215\244\250\231\352\026z\211\244\250\331\3522\365\031z\215\236\243/F\372\211\3335\013=F\315\3150\2650\2654\2654\265&\352]\324\341\203E8\032x\245\245\0035 LT\211\326\257CVUsV#J\262\261\324\311\035N\243\0252.j\312G\232\220%H\251V#\216\255\"U\204CV\0251R*\223V#\216\254\252W\230F\370\253\010\364\216sT\346\353\232\213u(jpz\265o2\251\371\205\027\022\2536W\212\204?\255H\036\227w\245\001\350\337AzizB\364\306z\215\244\250\232J\215\244\250ZJ\211\244\250\232J\214\311Q\263\324e\3526\222\230^\200\364\307z\201\236\230Z\230Z\232Z\232Z\2234\231\2405<58\032x\247\216i\336\302\246ARb\205\034\325\270Z\257C\315\\\215j\312\255<\014T\321\256j\334q\364\253J\234S\325*t\216\254$uj8\352\312GR\210\352DJ\260\211V\021+\307\204\236\2254sv\251w\346\240\224\344Um\324n\247\006\245\337H\\\232\262>h2G#\275F\262T\233\3517\373\322\356\244\337H^\230^\243g\250\232J\211\236\242i*&\222\242i*#%F\322TfJa\222\243/M/F\372c\265D\317Q\226\246\226\244\335F\352L\323I\240\032x5 5\"\342\247T\317\265.0jt\034S\261OU\251\343\\U\330[\025~#\320\325\201\315J\213\232\271\014|U\330\243\342\247\tR\244y\253\t\035X\216*\260\221\340\325\224J\224%H\211S\204\251\343Z\360\245\233wCS$\2305i\037\"\232\346\2539\301\246\346\227u;u&\354\032\235g%v\346\231\300\357K\276\215\343\265;x\355I\276\232^\2432Tm%B\322TM%B\322TM%D\322Tm%F\322Tm%Fd\2442SK\320\036\232\357\305D^\233\232ijM\324f\2279\244&\220\032z\232\225ML\206\254\253qJ\0075*\n\231V\245T\253\010\265e\022\255\302z\003WQ3\322\255D\225r$\253h*tL\325\204\216\254\244uj8\352c\037\024\344\030\251\325jdZ\231R\247H\353\347tl\032\260\222f\254\306\346\245c\300\250d\342\242\335\2127S\203PZ\233\272\245^A\244\335I\276\223\314\305\036e4\311Q\264\225\023IP\264\225\013KQ4\225\023IQ\231*&\222\243i*6\222\230d\246\231i<\332x|\323Y\252-\324o\2434\204\321\232\\\321\326\201\326\236*U\251\226\254%J\242\246QS\240\253\n\2654iV\321x\251\222>j\334y\025v\023\232\277\032f\255G\035\\\216.\225e#\305Y\216<\325\225\217\024\375\264\252\225:\245N\211V\022:\235c\257\232\310*y\247\253`\325\330OCS\261\252\362\265@Z\215\324\340\364n\246\227\251c\220r3H\347\322\2432SL\224\236e4\311Q\264\225\013KP\264\225\013KP\264\264\303%F\322TfJ\215\244\250\332J\214\311I\346f\232_\024\364\222\236\355\306j\035\364n\245\017N\316i3\212p4\361N\003\232\221EH\242\246AS\240\251\320T\350\265a\022\254\306\265a\022\255\242\000\265:\307\306sR\242\342\255@\270<\326\244\003\201Z\021%ZD\253\010\225f5\253\n\264\375\224\365\216\254,ub8\252\312GR\004\257\230\244}\346\233\320\212\271\031\332\005M\346qU\344\223&\242-I\276\224=.\372ij\217~\rX\337\362\324\014\364\303%!\222\232d\250\232J\201\344\250^Z\201\245\250\232Oza\226\232d\367\250\332J\215\244\250\332J\214\311M2R\371\231\247#\324\305\362\265\016\356iCR\251\247\251\301\247\236FE*sR\250\346\244QR\252\361OU\315N\213S\242\324\350\265f4\253H\225:%Z\215*\312\002F\000\251\322?Z\260\221{U\250\343\253\360\'J\321\204c\025q\026\254\"\324\352\2652\212\231W&\246\t\322\254G\035YD\251\325i\341y\257\224\267R\253d\365\251\326\340\214\017J\223\317\310\250\231\362i\273\251\013R\207\243}4\2751\236\244Ir\234\324L\365\031\222\232d\246\264\225\013IP\274\225\003\311U\332J\214\311L2SL\264\303%4\311L/Q\226\246\356\247\003\232\221x\247\227\342\242\r\363S\263\203OV\251\001\315J\207<\032z\256\rN\026\244QS*\324\310\225:\307S\"U\250\343\315Z\216:\266\221q\232\231#\253QGV\322!\216\225:GV\022*\263\034uz\010\375\252\342GW#\\\212\265\032T\252\2652GV\021*UNj\332\'\0252-L\027\212r\212\371\'u\001\261N\rO\017\212\013qF\352M\336\264n\244-M/Q\263\346\204~\324\326|TfJa\222\230\322T-%B\362Uw\222\240i*&\226\243ii\276m\'\231M2S|\312\013Te\371\247\254\270\251<\352x|\212f~j\223w\024\34452\032\225\0175eFj\302\n\221V\246E\253q&j\312\307\355R\244Uj8\261\212\267\034Uv8\270\305J\260\373U\230\341\366\253+\037\250\253\021\307VV:\2364\253\221\014U\224>\225n,U\225\"\247J\262\213S\252T\250\234\325\224\025:\212\230\nP\265\361\376\374u\245-\273\221\320Q\346d\323\267r*U`x\244\'\232i4\233\251\013S\031\2522\324\253\300\3156F\342\2533\342\2432\323\032J\201\344\250ZJ\205\344\252\355%D\322TM%0\311I\346\322\371\224\236e(zij\025\351\333\261S\304\324\245\271\251\024\344S\324\342\246CV\"\003<\325\330\370\351S\252\324\312\275*dNj\364)\322\255\"U\244\2078\342\247X\261V\243N\225n5\2531\240\253J\243\002\235\220\rH\216;U\204b{T\350O\245Y\2103\032\273\032b\244\335\264\342\244B\331\310\346\255G&:\365\253qL*\332J*eqS\243\n\260\246\246S\221R(\257\215\213z\232\2267\302\034TA\260\334\324\241\263\315(~})\305\275\r7y?J\013SKTl\324\300\334\323\214\230\353PI(#\212\254\362T-%1\244\315B\362T\r%D\362Ui$\250ZZ\215\244\246\031)<\312Q%\033\351D\224\375\331\024\014\346\236\017\025,MR1\311\342\244\211\275je\301\251\220b\255F*\334~\365m{T\350\275*\334q\216*\324k\212\271\032\325\350\0235d\303\200\r*\340T\310\330\251\321\316zU\224\311\251\304\031\031<\324\251\t\354*\322[\271\253p\3321#5\261ib\253\202ji\342\000|\242\250\264M\273\245^\266Q\2140\253&\327w+Q\230Y)\213#\206\305\\\216V\342\255\307!\305[\212L\342\256\306r*\302-|`i\276a^\224\007,j`\330\024\273\263C\034\n\013`b\233\272\220\267\025\03357uG#\324\016\365]\336\241i*&\222\241y*\026\222\242g\252\362IU\332J\214\311L2Ry\224\242Jv\372pzz\275J\0374\360jD\251~\265,uaz\325\204\031\2531\n\270\203\000U\270\327\"\255\"t\253\221-\\H\363V\342LU\310\260\265)\223#\002\234\210I\346\255\305\026x\002\257Ed\314>\355\\\207On7V\214V\034c\025n\033\000:\212\273\035\242\000\t\351S\013d\316EXED\357R3G\214\001\232A\0227U\251R$\037\303R\205\031\371F\005+\303\221\3275\017\331\207a\315*\246\323\310\247\026\njX\344\346\264\255\344\r\212\320N\225\361a4\323@\342\236\0374\340\3243d\nB\334\234\323Kb\233\273\212a99\246;\342\253\273\324,\365\003\275@\317Q3\324N\374Uf\223\232a|\212\255#\325v\222\2432S\013\323|\312z\311O\022S\203\324\213%J\257R\253T\361\265MR\306j\312u\2531\212\267\020\315[J\273\n\360*\364i\234U\270\322\256D\265eH\025&\342G\025=\274e\316\024d\326\315\256\230\317\202\303\025\255\005\212G\216+A!T\034\212\231B\2020*\30279\002\246POJ\225b=\352e\2074\357\263\234\325\224\266P2y\251\004c\037(\247*T\241)|\2726b\221\220\021\315f]\023\033c\265Ij\306LV\244A\243\301\355Z\2606\345\025\361ni\271\2434\241\251CR\223J\314\n\202z\324d\344\322c\202)\214\330\252\356\365\003\265@\317P;\324\014\365\013=F\317\305U\221\351\202L\212\206V\305Tg\250\214\224\205\351\273\351\301\352Epj@\342\236\257S\253T\310j\314|\325\220*D\0305f>\265r1\305Z\214U\310\3278\253\361/\025\241\n\360*\352\'\002\246^*tN\346\255End<V\355\205\232FA\3075\265\033\0000\005J\2715aA=jdJ\260\211VcJ\260\251S\242T\253\036\343Rm\344\212\220F})\351\027s\326\237\260R\204\240\306)\214\225\233{\016\3420)\326p\354\353Z\341r\200b\255Z\251\003\006\276+\315&h\316h\315(4\273\251r\017\007\2458\000\0055\333\002\252\310\371\252\356\325\003\265@\355U\335\252\006j\205\236\242g\252\322=B$\301\250\345\220\232\246\317\311\250\313\322o\246\027\245\022T\210\365.\342*Dj\265\033U\2045f#V\321\252d\253q\n\271\030\351\212\265\030\253\360\247\002\257\302\274U\370S\201V\301\300\251\341M\307&\256,E\210\013[6V\273PdV\254H\007J\264\213VQj\312-XE\253\010\225j4\251\325*A\307\025n\025\030\247\010\306\356*`\224\316\344\nz\212~\007\343HV\223h\006\240xC\222H\2428\300l\n\323\212\037\224f\247\216,\034\327\304\006\2234\231\245\315.h&\223u(\227\002\242\222M\325\0035@\355P;Ugj\201\332\241f\250\035\252\026j\255#Ugza\223\212\255#|\306\242-H^\232^\227p\251cz\234\020E=MY\211\252\322U\250\252\352\016*t\025n!\322\257B=j\354KW\241Z\320\201j\364`\001R\250\334\303\025\243\004Y\300\025\265kl\240\002\302\264c\003\265ZAVc\025f1VPU\230\326\255F\265j5\315YU\240\307\316j\334\021\222\265<Q\374\307=\251\345qP\221\226\342\244\002\245*\252\276\264\305 \212B\200\362M5\306@\002\237\005\271$\034V\200\033p*@k\341\243L&\2234f\224\032Bi7SKTL\325\0235B\355U\235\252\273\265@\355P1\250Y\252\027j\253#Ugj\210\265A+r*\"\324\302\364\205\351C\324\310j\322\221\266\234\244\346\254\304\325r3V\342\355Wc5j1V\342\034\212\277\020\253\321\014\325\370V\257D0EX\317LU\313u\307&\266,\"\334A5\260\270\030\002\254\305V\322\254\245Z\214U\230\326\255\306*\314b\254\3060EZAO8\034U\373|2b\235\267nsQHK\034\nhV\317J\231P\234qJ\352FEV(\301\370\351V\022\022\335j\302\333\201\326\254$x\245e4\252\276\265\360\333\n\204\365\246\223FisHM&i\244\324Lj\0265\013\232\254\346\253\271\252\356\325\013\265B\306\240\221\252\244\215U\235\252=\325\024\255\305BZ\243&\232Z\205j\261\033\325\205z\231\0335j#V\3428\253\321\034\342\256F*\324uv*\320\206\257\302:U\370G\"\255\216*\335\272n\344\325\330\027{\214t\255\353e\021\240\035\352\342\032\267\035[\216\254\307W\"\253q\212\267\032\325\230\326\254\306\274\325\2001A^sWm\270\305Y\312\263`\323\035\0009\035*\274\227+\031\250N\247\351\201B^\031\\\016\265\251\034a\324\020*\302\305\216\225*\246jU\2175(\204w\250\335\0247\025\360\223\032\205\2523Gj3A4\334\323I\250\232\241cP\271\252\356j\263\232\201\315@\346\241cU\244j\252\355U\2445\036j)\017\025\016x\246\023M&\224\032\225\rYZ\2363V\343\253\221\325\350j\364F\255\307\315\\\213\265_\207\265h\302:U\370\370\305N\2373\001Zp!\332\005hZ\307\206\025\250\215V\3429\253\221U\270\315Z\216\256EWb\355V\3435n>j\302\2561S\016i\315\320T\320\266*u9<\365\245\221\266\216\265\227:\231\033\212\317\2226F\255]&\r\356\t\256\215#\n8\247\242\234\373T\333x\244I6\2674\351&\335\302\324Y\311\346\276\024\"\241j\211\250\006\202qHM!4\302i\214x\250\036\253\271\250\036\240z\256\365]\352\007<UY\rU\220\325w4\300y\346\242\220\365\250\r0\232J*D5j6\342\246F\346\256D\325z#\232\277\r]\205sWc\\U\310\205_\204c\025\241\025]V\033j\335\252\344\346\265`\255\030\260\243\212\267\031\315\\\214\325\310\232\256Fj\334f\256Dj\344f\256EWb\346\255\240\315J\027\024\377\000+&\225>Y\000\253\312\200\214\212I-\367P\266H{U;\273\020\275\005K\246\257\226\370\255\325PE;\201F\340i\n\212B*&\310\351_\016:b\2538\305@\324\212y\247\036\264\323M\3150\232\215\215D\365]\352\026\252\357U\336\253\275@\346\252Jj\244\206\253\261\246u\250\330\324,j2iz\nAOZ\231\032\255E\315^\210U\330x\255\010{V\204#\030\253\2503V\343\030\025r#\216\265r)*\354J\315\327\245_\205\266\340V\255\250\316\rh!\305Z\210\325\310\215]\214\325\250\315\\\215\252\344MW#j\271\023U\330\232\257Ds\212\2621\201Rn\003\245B\315\363\325\250\346!1K\346\2615<R\034\202j[\225\014\231\252\021\270\216Q[1\311\205\006\223y,EH\264\356\364\206\230y\257\210\235A\025RT\252\216\274\3231\203JO4\323M\2465D\306\243sP=B\365]\352\007\025]\352\254\206\252Jj\253\234\325w\246\257z\211\272\324\rL\240\232L\324\212jx\352\324uv#W\242\347\025\241\017j\320\207\265hF8\2531\324\350{\n\275\016\027\031\353Z\020\313\221\201W\355\343.sZ\220\260A\216\365r3\232\271\021\253\221\232\265\033U\270\332\256F\325n6\253q\265\\\215\372U\370[5z\'\253(sO$\366\246\204\346\246E5:%Y\215*r\273\220\212\307\234\025\227\217Z\320\266\237\200\255VP\202\306\247\024\242\202j2k\342f\025\013\256ER\225pj\022\265\023\036\271\246\253v4\036\264\323Q\260\250\232\241aP\270\250\034Uy*\244\206\252\310j\244\206\252\271\305@\335i\005B\335MB\324\312c\036h\024\365\2531\325\310\205\\\217\222*\3645\241\rhC\332\264\"\346\254\216\005K\t\3475ad\313`V\275\234D\201\232\327\215\202.\005O\023sW\242j\273\023qV\321\252tj\265\033\325\350\236\255\306\325r&\253\221\265]\205\361W\241|\325\304l\324\3523R\005\305J\242\254 \251\327\212\232.s\364\254\353\210\201r})\321\240\0035j\023V\001\247)\244\315FO5\361ST&\253\310*\253u\250$\353P\323\263\221\357HzS\032\243aP\260\250XT\022\n\247%T\220\325Y\rT\220\325g\250Z\232;\324-\336\230i\204TX\311\251\222\002\325!\210\255=\005Z\216\256\304:U\370E_\207\265h\300j\364b\245-\212zK\205\253\226\274\260&\266\241\220\200\000\253\220\271\'\232\275\021\253\261\265\\\215\360*\312IV\021\352\334MWaj\271\033U\310\232\256D\371\253qI\316*\374M\214\032\277\t\357V\343~qVP\206\251\002\355\251\026\245\335\201K\024\303\232\245qq\202\331\357L\212\1770\205\025\243\027\002\246\315*\236iI\353Q\232\370\271\226\240aP\270\315T\221H\317\245U\177Z\214\212fpi\364\204TdTL*\027\025VJ\247-S\222\252IU^\253\275B\302\233\332\2414\334Pc$Tay\253\260\201\306jFPA\305F\253V#\025r!W\241\253\320\326\2045v6\300\247;f\233\031%\200\255{a\2001Z\220\267\025v\023W\242j\267\033U\224z\263\033\325\250\332\256\304\365n\'\253\261\275[\211\352\344MV\324\364\305^\267\2238\006\264\340|qVP\374\325a%\305^\211\204\213N+\264\373S\325\014\247\013J\366\376R\234\034\361X\272\211*F*]92\0015\256\235)\333\250\r\3158\2650\265|j\353P\262\324,\265\004\211\220j\204\313\216\225\\\323\030R\253v4\374qL\"\243aU\344\252rU9j\234\225VAU\\Uw\025\t\024\323\322\242\"\205\\\323\312\361L\331\315L\203\024\346n\324%X\214U\250\352\364=\005^\203\255^\216\254\253b\236\032\226&\303\214\326\265\273qZ\020?j\277\023U\310\332\255\243\342\247G\2531\275\\\215\352\334oW#j\273\023qW\"j\275\023t\253\2617J\264\234\034\212\275o!$V\202\265L\206\254E!C\221Z(D\210\rIn\n9\367\251\'\031J\306\324\340$\006\247\331\r\250+@\032\t\244\006\234O\025\021s\234\036\265\362\003-FR\243h\352\007\217\212\316\236>MTe\301\246\025\246\355\3475&8\246\225\342\242qUd\252rUI\005T\220UY\005V\220Uw\025\013-4\255D\313\3159\006\005;nW4\230\246\223\212h952\n\263\030\2531\365\253\261U\350{U\350\315M\232x<RFI\220V\275\273`\n\275\023\362+B\027\253\221\275XG\253(\365j7\253Q\275\\\215\252\344OW\242|\325\270\236\257\302\365v\'\253\261\266j\334M\202+F3\275x5<G<\036\265>p*{{\222\204/j\322I\200 \372\324\3228#\216\365\235\250\260\362\300\250mxQW\003Rn\245\315.\352%Q\267x\355_#\262TE1Q\225\250]*\224\361g5\237\"`\363P\225\246\225\247\017J\030qU\344\252\222UI\005U\220Ui\005U\221j\263\255Wu\250\212S\031j=\2314\360\231\340R\274ei\241)\222\257\034Th\265e\026\254 \251\343\034\325\330\252\3545r:\237\265*\032\222 \004\2315\247\t\343\212\267\033t\253\3617\025n6\253(\325b6\253q5ZF\253q5^\210\346\256D\330\253q\275]\211\352\364Rr+B\'\253\221\232\275n\3705qFy\035j\300\345pjkU\035\372\325\254g\247j\236\',pz\n\316\324\244\314\241i\320\034(\251\367Q\272\200\364\355\324\273\262\214\t\342\276Rd\250\214t\302\225\004\213UeL\326t\361\363U\331)\205=)v\2201Q\222T\340\364\250%\252\222\n\255 \252\262-U\221j\263\255Wu\252\356\265\036\332\215\222\221S\034\324\210\203\322\234c\315F\321\342\242e\317Zb\245N\213S\250\002\246\214d\325\270\305\\\212\255\307V\0074\252>j\220\374\244\032\277n\331\025n3W\342<U\230\332\255+T\361\265[\215\261Vcz\271\023U\370\037\025y_ T\350\374\325\310^\257B\375*\374RV\204\022\014\001V\243\223\232\320\212L\212\264\255V\"85e[\232\231[\0035\215;\371\227\'\332\255\306x\2517PZ\223u8=9[\203_-\236j29\2460\025\021Pz\325yS#\212\243,95]\355\375*/+\035i\254\265\004\251\306j\254\202\252\270\315Wu\252\316\265Y\326\253\272UgZ\201\326\243+Q\224\311\241\223\260\247\252b\234x\250\\\346\243)H\027\025*\n\223mX\211qV\220U\250\352\334uaE?\035\351G\315\305]\267\\\001Wc\253q\232\265\031\251\321\252\324f\254#{\325\230\332\256\304\365v\026\253\250\374T\350\374\325\330Z\257D\365z\'\253\2615]\215\352\344S\205<\236*\364R\206\344\032\262\222b\254$\231\247M8X\2175\231\021\313\226>\265y\033\212v\352M\324n\2405H\255_1\025\250\312\324l\265\023\n\211\327\212\256\311Q\025\333\332\240t\311\250Y*\t\027\212\245\"\365\025U\306\r@\353U\244Z\256\353U\335j\264\213P2Te)\004tyy4\273*6\\\323<\274\363OX\367R\233zO/m=V\246AVPU\224\025f1VR\244\305 \030j\277\017J\264\206\254\304j\334f\246CV\024\342\245G\2531\275\\\211\353B\007\343\236\225ie\315X\215\363W\242z\273\023\325\370_\245^\211\352\344oR\003\271\261\232\323\200\225P\005[V\251\267\0208\252SL\354v\223SC\300\025d5.\3727Q\272\22459Z\276me\250\212\324l*&Z\211\205B\313\326\242d\250Yj\027Z\253(\252R\202\017\025M\371j\215\226\253\272\325wJ\257\"UvL\232\211\322\242\331\223Jc\300\244\tA\216\233\345d\322\230\251V,\032\231\"\315#Z\363Q\030\n\366\247F\234\325\225Z\235\026\254GV\024T\242\223\034\325\2503\212\271\0375f>*\312t\251\220\363V\024\324\250jt`*\334-WRLU\230\3375j\'\301\253\360\276j\364.\001\031\253\310\300`\203Waz\275\033\361Vm\206\351FkMxc\355J\267J\247\031\251\305\300\333\234\325r\333\3375e\016\005I\272\227u.\352]\324\241\251\352k\347\246\207\212\201\243\250\314u\013\2475\013\'\265B\353P\260\250XT\022\n\251 \353U\312d\034\326s\256\030\212iZ\211\322\240d\252\322&N*#\026\005B\351\236(\020\200=\351\217\035\'\227\212<\2726zR\371t,y52E\203S\371{\227\336\230\326\371\355Q\233r\247\212x\216\244T\251\025pjt\025 \004\324\210\207\251\2531qVc\253h8\0252\364\251\022\247S\300\251W&\247\214z\325\244|t\251\343z\271\023zU\3049\372\325\270d\305]\211\352\364RU\350^\257\305%^\265\227ksZ\261\220Ww\255S\272\217k\006\007\203R\303\222\006MXQ\212\2206)\341\251CS\203R\346\234\032\236\246\274\r\333\002\253\261\250\331\261P3TMP\262\346\240u\367\250\035N*\006\004\216\225ZD89\252\344qT%O\336\032n\312\215\226\241t\315Bb\357PH\230\246,<d\3222\034\323\032>:Q\345\361Hb\243\312\366\243\3134\242<\032\220-9x\251@\004PR\233\260f\245H\201\024\361\020\251\025\000\251UG\245J\0234\021\264\200*\314c\246j\332\216)\352jU\251\324\360*t5:5J\246\245F\253\260I\212\273\033\346\255\306E[\214\325\310\236\257C%^\211\352\3442a\205lE\'\356\361Q\334I\362\250\245\205\361V\203\346\2245?u(l\323\203S\203S\303S\201\257\004qP\271\250\035\352\273IQ4\265\023\313\305Wi*&\222\231\2734\307\\\212\252\311\212\253:\014g\275C\214\212\215\226\240n)\215\310\250\032=\306\244\021|\265\031\206\230c\244\362\275\250\362\250\021\212_&\223\312\244)\212P\225\"\255?o\024\322\234\323\324b\236)\300T\213\301\251\327\326\243\335\271\352\304g\221V\225\252E5*\232\225ML\206\246S\306jEl\325\2045b3W\242n\225r6\253\221=\\\214\325\330\217J\271\023\325\244~\365~\332\344\343i54\222n\024\350\332\254+\324\241\251\341\251\301\251\301\251\301\251\301\252Ej\360\211\rU\221\252\244\215U]\352\026z\211\236\242f\246\023\232r\212y\\\212\202H\370\252\222G\236\265\003\304T\373S\032<\212\254\361\020i\2062h\021\340R\355\342\243d\250\331(\tHc\244\362\361I\202)z\323M\"\216j`\005;\024c4\230\346\235\212x\024\341R\216\225]\270\223\332\255\305\332\255)\004{\324\253R\255J\2652T\240\372\324\253S#U\210\315[\215\252\344MW#j\273\023\325\350\232\255\306\325e_\212\232\027\303\n\272\357R#T\352\365*\265J\032\234\032\234\032\234\032\236\032\236\255\315x\\\246\251\311U$5Y\315@\306\242&\232E&)V\246Q\305+G\362\232\241\"\340\325yNF)\241\306\323\270T$\206\351HPb\232R\233\345\346\220\307M0\346\232\321\342\243+\212i\250\230\3233M\335\212i\220\n<\352p\236\244YjU9\247\250\251DD\364\247\213f\245\362\266\3655\033\305\236\2254Gh\346\246C\310\253IR\250\251TT\312)\343\212z\265J\246\254Fj\334g\245[\215\252\344mW\"j\275\023U\330\216j\322\216*x\370 \324\373\363R\306\334U\204j\231Z\244\rO\rO\rN\006\236\r<5xt\225RZ\247%Vz\201\2522(\305.\332\002\363VR?Z\216\346@\243j\325\006\250\035\t\250\231\030\016\224\301\031\317\"\237\266\232\302\232\0074\2458\246\221M`\030Ui\001\006\242\'\025\003\232\205\237\025\033IQ\027&\22015*\006c\300\253Q\333\310q\301\253q\333H\007\3355e-\237\031\332i\342&\035\2158\254\200t5ZF#9\250\222|\036j\310;\271\025*d\n\271\031\340T\340T\242\244\006\236)V\246Z\231*\324f\255\306j\334f\256D\335*\364M\322\256\302\325v6\315\\\214\003\031>\225\t\233i\251b\270\025n93V\025\252e4\340i\340\323\301\247\003R\003^+\"\3259\227\255Q\220u\252\316*\002)\204Q\212Z\232\030\363\311\245w?\303U\232&s\223J-\375E\006\334SL\000\366\250\244\200\016\325Y\223\031\250Xg\2450\214\032p4\326\3053\245G\"\203U\\b\252\310*\273)4%\273Hx\025m4\247n\306\255E\242\271?t\342\265\255tE\030$\001W\377\000\263\242U\343\255=mbP8\247\030\223\267\024\326\265N\242\201j\230\346\250]X\214\235\2039\254\271,\212\234\343\024\350\240}\330\305h-\266V\244HJ\365\253\n\274T\241x\244<P$\002\244\017R\253f\254Fj\312\036*\304mV\342j\271\023U\330[\245]\210\325\310\332\256B\340\002\017z\204\304\314\307\236*E\201\207\275X\2100\351V\243\223\326\255#T\231\247\003O\006\236\r<\032\361\347J\2472U\tV\252:\324\014)\204R\021M5n\022\014|S\322\035\334\323\314@v\246\230\352&\\TG\255\014\241\205U\232.\rS\333\202j\0318\250w\363K\234\322\212cT\016\231\250\014\005\317\002\245\217N,\303ul\331\351\n0H\255d\260\215\000\342\236#U\340\nF8\246\022MF\331\246\362)7\237Z\004\236\364\355\343\275C2#\203QG\030\317J\262\024\001\236\364\207\031\245Z\220t\250\035\300\004US!\315M\034\276\265n6\310\253Q\265ZC\232\235\rY\211\252\344mW\"j\275\023U\330\333\245Z\215\252t\346\254%K\264u\035i\244\367\035EK\024\231\253hr*AN\024\361O\006\274\226E\252\223/\025\237*\3259\026\253\260\346\243\"\232E4\2415,D\250\305_\200eO\024\346\004\236)\276Y\357P\312\270\252\376]!\\SfO\220\326pB[\030\246\334A\204\310\025\232F\032\244QN#\002\230FjH\355Z^\202\264\355\264\262{V\202YG\037Q\315M\200\275)\215%1\237\322\243$\236\264\224\322i\204\323MFN)7R\026\240>:S\274\312PjE5 <Uk\210\333\250\351U9\035i\312\325j\027\307Z\275\035\\\214\234sS+U\230\332\256Fj\344G\245]\211\272U\330\333\212\265\033U\250\332\255F\3258\366\244x\311\345z\324`\0259\253p\311\221V\223\232v1N\006\234\255^W%S\230U\031\207\025FQU\210\246m\315K\0349\353S}\234c\245:;M\307\245\\Kr\203\030\2462\200x\245+\201U\234\014\363\315D\330\003\201P2\346\227fF\rD`U\344\n\255r\000\214\326)L\261\251\025qC\220)\221\251v\300\025\277\247\331\034\002\303\025\247\263`\300\250Y\252&&\243\3154\232i4\322i\214i\231\244-M&\230i\271\244-NSR\003R\003R\006\300\247\3440\301\2523\306\003eEB\005O\0305~\002E^C\305=j\314f\256Fj\344F\257Dj\334mV\320\325\250\332\255F\325j3\232\231i$L\214\325p\333\032\256\305!8\305M\311\247\000GZp\257.~ES\226\250\313T\345\025\\\216i\361C\270\364\253\211\016;U\210\355\363\324U\330\241U\035\0056\343\n\274U\0227\034\323%<qU\230f\242aM\305(\024\307\\\212\317\272S\264\326[\020\246\2432zS\222&\224\360+Z\306\304\002\t\034\326\332\3425\300\250\244\2235]\230\323\r4\221L&\243-L-L-L-I\273\024\322\364\335\324f\2239\247-H\r<5;4\345jl\204\032\210(\31752\001Vc|U\244\222\245\r\232\263\033U\270\216j\354g\025r#W#5n3Vc5i\rZ\215\252\302\232y9\025VU\301\247\301!\007\025u\0375(j\007^+\313\244<U9MT\222\252\310*!\036Mh[\301\201\234U\225\213=\252dL\nq<{UY\316\343\201\332\242\333\305C \250\030Svf\230S\024c\212\215\306*\205\3361Y\023\250\'\345\346\231\024\005\210\310\255kx6\340\001\212\325\210\010\320b\202sQ\265FsMcP\263Tl\325\031j\214\2654\2650\265!ji4\204\321\272\2245(jpjw\231\212p|\323\303R\026\024\200\363R\253\001\324T\361\310\t\025e*\302\n\260\234U\250\232\256\304sWb\253q\232\267\031\253iVP\325\2045e\rH\r5\306G5]xz\275\031\310\340\324\240\323\324\327\226\311U$\252\262T\004f\247\267\203q\316+M\"\332\240S\266zP\303h\250\334\361\216\365Y\2074\323\300\252\322\236qQ\3434`\ncS0*\0311\315P\226#)#<T_b\000\363V\342\265D\000\342\234\355\260\361\214\323\321\313\032\224\360*\031%\n\t5J[\325\\\363U\216\242\t\347\245\037o\214\322}\241\033\241\243x=)\206ALi*3(\246\371\264\307\237m@\327\200t\250\215\343\036\224\365\272jx\270cS\244\247\034\323\213\023OV u\2517\232P\371\251\343\367\247\367\251U*\314d\216\265m=j\302U\210\352\354]\252\354Un3V\3435j3VP\325\2045f3S\nq\031\025VE(\331\02542U\260r*E\025\345\222\032\251!\252\315\315\010\233\230V\234\020\355\025c\024\207\201\357Q1\342\240s\324\324-Q\310\330\252\262\034sQ\027\003\2554\315\237jkJ=j\007\271\003\245R\271\272\003\241\346\243\206\350\036\365ed\337R\006=*\'\0074,\273?\n\251q\251\225$\003Y\263_\273\367\252\215#\267SM.GZa\227\322\201pGzQz\313\336\246[\364?z\246[\204q\301\246\271\003\221P<\307\265U\222V5\017&\246\215\t5m#\030\251V1R\252\323\302\212\177\035\351\014\253\234R\207\031\251\221\352\302\266MXJ\260\230$U\310\207j\260\023\214\212\2321\203W\"\355W\"\253\221\325\250\352\324f\254\245XJ\260\206\247S\232\220sL\221r*$\0305n3S\003^U)\252\222\034\324[rj\345\254\005\233\245i\210\266\212B\270\344\324NE@\354*\273\032\211\217<TL3\326\253L@\2522IP<\270\357U\236s\330\325y&n\325RB\317\234\322\300\010`+^.\000\251\2529\016;\325\033\213\225@@\353Y2\271s\232`J\016\005W\222AU\332J\211\244\250\332Jn\372U\235\220\360jU\275q\324\346\236/3\326\227\317S\332\236\222\'z\225$L\361VU\301\357R\006\003\275)\235W\2750\334g\356\320$8\347\232\003sS!\253\t\323\212\261\031\253Q\232\265\027QW\243\343\007\265[A\221NN\265n*\275\025ZJ\265\031\2531\232\265\031\253)S\255N\225*\322\260\310\252\303\206\305N\207\025aMyT\2035\027\226X\361SEhI\344V\224\020\204\035*f\340sU\235\262MV\223$\324,\246\242~*&5\014\207\000\346\263.%\3118\252N\365]\3335\021\353N\n\034`\323M\267>\324\242\r\274\212\235\034\255;\317\342\232\356XqY\362\332\263\234\365\250~\312GQQ\310\205\007\002\263\347v\006\252\263\032\211\230\324Li1\232pBi\336Q\243\3114\322\230\245\tN\013\216\364\360\017cR\2430\357Roj\006M<\034T\212\325:T\3501S\243m\342\254\241\315Z\214U\310\305^\217\221VS\245=z\325\230\352\344F\256Dj\334ua*\314f\254\2475a*e\251\224\324\202\241h\362\331\025 N=\351\352ppk\314\266n5f;lu\034\325\250\341\305M\267\002\241\223?\205C\345g\223\322\241p\240\361U\337\212\254\3475\013\266\001\315f\334\317\270\220\247\212\240\355P75\033\na\024\200\342\232\327\005j#r\344qP5\313\203O\216g|U\264}\243\236\264\3573\'\245)\301\034\325y\242\3348\252\022\330\263d\325\tm\031{Usl\304\364\246\233F\364\2446\305\006H\244\001\207j\031\210\246y\236\246\227p4\322@\243u=ML\206\244\342\214zR\212\221jt8\251\225\252\302|\334T\311\225<\325\350Nj\344g\326\257BF*\324~\2250J\2321\212\263\035[\214\325\310\315YCVR\254!\305XCV\026\244\007\025\"\232\\s\232\224\014\212G_J\363\310b\311\253\313\026{T\2420)\257\201P\2200Y\272U\013\233\241\321zV{\316{\032\256\323\222y4\323.{\325;\211\267p\275*\213\363P\260\250\210\250\332\231\365\250\335\2608\252\315\315>%\365\251\232\335[\266)R\035\2751N8\007\223L/\375\334\032\215\213\372\322y\244pM5\3468\342\253I.z\212\256d^\324(\311\245p\270\347\025VIQx\025Q\2432\234\203M\373&:\265\006\334\216\206\220@\304\324\211o\357R\224T\036\364\300rx\251\000\247\201NU\317J\225P\324\252\2652-L\203\025j2\017Z\267\020\002\256\304GCV\343\307j\266\225a\rL\242\254F*\312\n\267\021\253IVP\325\204\251\320\325\2045.x\245W\251\224\346\245^:S\310\334+\202\206,b\256\252`R0\252\362\270S\317J\314\274\273\317\312\275+-\344\311\252\362IPn\315#\023\212\256\365\003TMQ0\250\332\242j\256\346\241=jH\363\326\244iO\255F\323\036\334S\001$\362i\031\310\350j=\355\330\322\035\346\242e\220\372\324N\204\014\271\250|\345Rr*\031/\030gh\305T\222\351\333\275W23\032\2269\034p\rN\245\217Zx\311\247g\024\231n\302\201\0339\346\245\020m\353N\tO\tOT\"\244\037J\225@\251\227\035\252U\251S\212\265\031\253Q\232\271\023U\310\332\255Fj\312U\250\371\253\010*x\3705r3V\022\254!\253\tS\245H[\212@jTj\262\255\305H=\253\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\010\026IDATx^\355\335\333\266\243(\020\000\320^\231\377\377\344\311\232\311\255\317\211A\243\010J\301\336O\335&\022\254*\020r\351\376\363\207q]\247\007\000\000\000\350\323ez\000\000`\216w\216\001\000\000\000\000\240y\276\007\320;\037\330\000\000\000\014\3046\037\000\000\000\002\261\221\357\231\354\002\000\000\000\000\000\000\020\224\237\247\237O\016\000\2003\370\352#\000C\370\262\351\256}?\254\335>\000\000\000la\237\272\326\2277\024\2003\031\240T\241\260\000\200\026\014\277&\271\016\037\001\250\301\300\002\000\200q\244\277\022p\271\244\217\263\235\035\026\000\000\020\200\255K\232\270\220\2440\350\225\332\006\000\000\000\000\000\000\200\306\370y\017\000\300D\344\005\222oj\026\220]\000\201\242\237}\215\335)\023\211@\251\007\240Weni\014E\321@P%\266\037\327\177\247G\3063\356$x\235)\242q#RV2\270-\222pT\001\037\224\004\014\305\220\007\310Sl\323\227?\021\347\237\311\016\037a/V\nG\t\327a\000\240\214\217e\014\000\000\0004\315Nv\007\237\005@\017\214d\000 \213\275\024@\030\246l8\235a8\250\265\211_\373<\000\000\200\265|\017\000*1\270\000 &o\304\303VF\r\0000 K \000*q\2139\316\330\261\366I\346\237\177\246\007\016u\271\304*@\005\223ic\340bU\305(nY\t\233\231\260\035o\207\020\016@\222a\025Ce\233\237E\340\306\345`\253\024\300\031\352\025\317\346\226\317.\200g\2077\367\033\316r/V\025\013\035+6\300\2135\304\261v\254\215\2469\23745}\030\350\336\216\t\205\346\310\346\300\336\223\177\335vCO=;u\254\022\205;c)0\357\371\271?\363\357\323\227\316\333\250`S\354\224\316E\372\350 \016\234\2456(\220\222\307{z\253.\257\300\253\235*z\377w\233\317\362\374#w\303G\256o\337\322\373\355\361\275\276T\037a\234\236\311\332\245\332\261\323s\027Q\007AK\r\231\313\333\205\335\277\352\237zZ@M\\F\023\235xIv\246\334\257;\236\205\264k\240\354:y<\305r\307\010\272\036]?\027WnJ\353\315\257\002\350\272\026\000xi\366\226X\362>\324\354E\236huLJ&\342L\253/x \367\334\326\nLf\273\275\324\333\350.\271\005\020\226\312\205\035F\2330\000\206\222\275Jrw\250Fh\271d\017\3142\376\326\340\311\375\240\0029\005\240\tV\274p\234\357\343\355\3733b\n\264\364m\246\253\315t\204|\257\001\375\1772o?/~\3765\177\230\'\213\"\2779\340T\311\021\r\000\014\315\372`@_\222\036{\303\367\345\342z\365\223\264\330\351\353\234\344\214\315\017\3449\314\255\326\006\275\035\002\360#\275\364x\334 \334&\016\220N@}9\257\233s\016\007\311\034\255\263\247\315>\300(\214\367C\031q\000\000\235K\254\257\217Z\003\336^:\361\362ws\307\227\034\325\357~\345D\035\000\212r;?E;ao\247\'\024$\255\300K\353\363\201MqE\333\223/\035\345Mc\272=+@H\323\301\177\214\274)&\357,\032\2646\225k\237G\001\347\314\005\234\312\010\033\314$\341\341\006\275\202\355LN\005*\202\200$\215\202r\346\r\326\020Y\240M\226\021T\244\274\356\002\205\241\314r\245L+\r+\221\321\022m\324\327}*\363\304H\036\000\000\300\273\373^\346\366\357V\333\353m\323\326&pO\366\236W\262\247\t\222\202\204\364\270R\016\022\020\000\030\213\033t1\307\255\252\226Ii\023Z)\207\251\352\375R\177\215\223\240\261]\025\300\030\344\231e\325\027\003\023\275Wd\357\327GS\224\333fG\317xU\225\310\177W\001\001\322\322\003\3752\367\000\235\221f\036T\302\340v\177\003\354\262\343\\\032\223\223\312\234s\310\'\336T\245\300:\363\270\307_[^\353\265\334\267\320\226\003[r\250\337~I\002\214hy\236!\214\334D\346\236G\'\024\000@\024\211\031\273\3517\t\250\344\261s\177O\275B\030\234\002(\372\366\0304\250B\211Wh\222\352L\367\274k\254\"L+\301\224I\330O+e\332[\322X\305\237\353\025\214\374\240\024M\235w&\026\255\213N\201<\254t\334+\361\260\256\002\310\"\270023\000c\353\177Ig\214\337\t\303\340f\n\340\232\271\007\337=q\354n`^\305\246\343\232I\263X\3018f\246\001F\241\000\000\370\253\231]@3\035\031\324\001?\370\336\370\n\326+\017\033\303\026@\177W\004\001\031\210\320\260R\003t\266\235\331\007\0060\304\002s\335E\256{\026\364c\300\251\3170\347I)\3600\340<\310/\362\017\034\312\244\003P\313\3103\254\315\355J\267\"y\025J\253A\363\177R\325\326j\346\001\000\0006zno\326\356r\326>\017\000\200#X\235\215\300\'>\207z\206\273\335\261\365\3363\325Q2S%\333\002 $\267\002\000\240+\3365H\213\037\227\330\313\326\370\361\247/\261\307\023@\014\346Z\232\024bY\372\326\311\020=\216\342o0MP+\tT\033^\225k:\330+^\004\357\377\213E\274n\003\005\271\027\227 \212P\2261\005{X\3363u\275\225E\370\302\010\177\001\'\022;\240W\366\r#X\270\213)\000\200w\013S\346>U\032N\314\342\211C\213\266>\277\204\353\031/\332\233IA\025\ti\221FV\2512\034\006\261\030\273\305\007\233\361\253\320bt\270\005\223\321\371\036\270\2310~\214\350\327\201\217\007hV\361\\\335\277\330\004\204\264yB\230\216\367{\003\323\2034\346\232\221\351:\224\312I\236\201\377)\203\234L\334\316y\235\367j)\247\035\372qidf\341`\006>\360\303\2140\n\367\374\215\014\r\030Q##\377\327\367\320\357\223\267\217oZ\223*\224\337\331I=\276\305[\332\211D\326F\267\267\002\366N\036@4\257Qo\364\277\023\217\256\364\234\316\2757\376hF\273\336\257\366\006\344q\376\3077\345\247\177/-{L\326\356\330\313\257\016\036\365\222\247h\372\342\026\252\244\351~\227\262p\375\364m\210\372\246\032\365\003\301\275\177&\367\366\207\rr\316iC\334\236\307b\235\231\253v\205^sr\263\241S\031\2553\006\245\001\000\000p\246*\273\262*\215\022\301\206\367\212\032sV\321\306\215X\302\245\257\313\201\263\2345\037m\027\247\247\241\010+\320,\377\034khU\263W\265\361z\202v\273\025\327\217\355\354\256o\232U\2605\301[\237\017\254\031\354FV\347$x\237\250\361{\374\3703j\357\331l\315l\017\324\267b\3325\\w[\021\345\272\016\313\341\351W\312@\016+kj*\232F3\320\340\024\300\331d`\352m\206\353?<>\351\345\215z\340TE\227\230\305\233\343\253\3523\310\363\277\240\370\370\247(\327\252\336A\362\345&\265\260 \353\242\030\275<Y#%\005\361\230a\000\330\343\324E\330\334V\261\263\233\333\221\2273FDYo\246\"\200.|\033\341\337\036\007\000j\330\260\375r\263\036\333\206R)h\356\235\030 \251\346@\255\3316p\276\364\030O\037=\312\271\257>\246I\314\367\245 \347\354\337\337\251\3139\177L3\221Z\273\216^\216y\342\320\321\326^\010o.{\343\266\367\374%\305\276=[\252\235\n\372\330\311\026\313\324/\345\002\363\210q\271\366\272\262=sa\003\031\266\343Um/\200C|&\253\321\216\306St\t\377\'\225\252]\n7G4\351\002\330]\246p\223./\226\030|\203Z\231\370Rc*\361r\245\232^\345\320\027\013\344\371c\371\300\266\365\277\306\033\032\215\032\347J\001\236\252M|\026\021P\201\201\005g:{\004~\274\376\307\201\031\033o\366\033\237\036\337\177\034\3615X\n\320m\016\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-5.textpb b/core/res/geoid_height_map_assets/tile-5.textpb
new file mode 100644
index 0000000..0cb5489
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-5.textpb
@@ -0,0 +1,3 @@
+tile_key: "5"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\325\210r*\326\332c\257\245S\272\203zq\326\271\353\244*Nj\203>*&;\272S\243OZq\217\212\253\"Ug\025\021\025\033.*\"\265\023\212\254\313M\021\344\323\230m\340TMQ\363HN\005D\304\323w\221NI\260y\251~\320\244t\250\245UpNj\250\201[5\024\226\313\216*\224\220`\324~MH\251\212\260\210jeLv\247\010\363\332\236\"\245(\007|R\205\007\2758*\216\324\274P>\224\360=\251\340T\210\2652\n\235\026\247E\253q\255Z\215j\344kW\"J\267\032U\224\\U\204\025:-[\215j\312-Y\215*p\000\247n\354)\342\244\002\245U\251A\342\245^+\224H\360j\310N)\205EB\351\3075\215\250\332\203\222+\233\271\214\2515\004g\346\346\256*\361JEV\225j\233\255DF*6\025\003T.*&Zh\030\2465D\334\232i\024\302)\214\265\023-3i\245\333A\004\212@\215\332\227\313\343\232i\266R3P\274\000S\004C5:E\236\3258\207\003\232]\241z\322\023L\306\343NU\003\245<&z\322\371b\236\020R\355\245\013R\252\324\252\265:\n\260\213V\243Z\271\022U\310\222\256\304\265i\005N\202\254\"\325\230\326\256D\265e\026\254\242`PF)V\244Z\221y\251\207\025\"\325\210\327&\271\245^j\302\307\305F\361\324.\244\203\212\315\271\214\220w\n\347\357m\216N\005d\264e\036\255\306r\005+\n\255\'5]\222\240u\250\030TL*&Z\211\205FE7mFS\232F\\TdSJ\323\nR\010\251|\272M\270\355Ml\343\212\210\356\244\332\306\224D\307\255H\260\016\365(@:\n\010\250H$\346\223\031\245\013O\tO\000S\250>\324S\224T\2503S\252\324\350\265b5\253q-\\\211j\344b\255\304*\312\212\235\005Y\215j\324kW#\025e\026\254\252\361\315F\364-J\24352\214\np\346\254D\274f\254E\367\253\237)\212\221\007\024\025\315@\311\203Un\242\014\206\262\036\000\344\202+\026\356\334+\236*\262\246\332q\034T\016\265\023/\025]\326\253\270\250XS\n\324L\265\031ZB\224\302\224\306J\217m!ZM\224\340\224\326\300\250Z\233\326\215\236\224\345^\330\251\004g\256)\3338\351M)L\332O\322\232\313\212hQ\353K\201J\0058\n\\R\201O\013J\026\246E\253\010\265:-X\215j\324kV\343\025n!W\"Z\262\213V\021j\314kVcZ\266\202\254\247\025)l-E\324\323\305M\030\251\324qNQS\257\240\251\342Z\306)\236\324\340\230\024\205j\027L\325k\205\371Mf\204\371\3533P\207\014H\025\224\313\212cTn8\250\034Ug\025\003\255FR\230R\243d\250\212\321\266\223m1\222\230c\246\371t\322\230\246\232\211\206j\"(\013R*\342\237\200)~\224\264b\243lTE3\336\223\313\367\247\005\024\360=\005;fz\321\263\024\273i\333qJ\00752-XAV\021j\302-Y\214U\250\305\\\211j\354kVQju\025b1V\243\025j5\342\247QO\3054\256\r9EJ\234T\353\315H\242\254 \253\n0++m.\332aZ\211\205W\225A\0305O\311\352k#P\307#\275bH\274\232\211\2050\256j\'Z\205\324T\014\242\233\260\036\264\326\213\025\023GP4t\315\264\025\244+L+M\"\243e\250\231i\205i\205i1\212N\224\233\21581\247\002i\371\310\246\220\r0\250\244\331\232p@)\352\276\325 L\212]\224\334{Rn\\\342\236\005L\202\254\242\324\350*\302\n\263\032\325\270\326\256\302\265u\027\212\235\026\247E\253\010*\312\n\262\203\212\231jA\3158\256G4\320*U\251V\247\214U\250\327\035idp\200\346\251\021M\"\241\225\266\203T^\340\346\2432\231\010\024L\010\210\342\260n\306\342k6H\352\006\216\243d\305@\365]\306j\026\024\312z\034\360i\255\035@\321\324L\224\320\264\205i\245i\205j6J\211\226\241aL\"\223\024m\244+F\332p\247\204\310\250\330\020x\244\301\247\001OT\024\360\230\245\342\223u79\2441\344{\323\343\007\241\253(*\304b\254\"\325\210\326\255\306\265n5\253\221-\\AV\021jtZ\235\026\254*\324\353R\255J\264\343\300\246\255J\005L\213\221V\243\000\n{\316\261\257^k*\352\3739\301\253\246\230\307\025\237r\3475E\315\020\374\315\305Y\237\0011X\2271\362k>D\250\031*\274\242\251\3109\250H\246m\3155\242\250\310 \324\313\363\255F\361\324\r\035G\267\024\025\2462\324dTl\265\013\n\211\226\230V\223e.\312M\224\205iU1Noja\244\300\245\340R\206\024\027\250\313\023N^jUZ~\336iv\340\203S\3063V\221j\302-Y\215j\324kW#Z\271\032\342\255\"\325\224Z\235\005N\202\245\025*\232\221j\314c\212k\036iTT\261\256ML\\ \252\322\337\204\350j\224\267\245\272\232\253\270\312}\253\245<TN+:\340u\252R\016\rE\014\233d\253\023J\010\254\371\271\252R/5ZU\252r\257\006\2522\234\324e)\230\307Zi\344Te\t\247\306\2705#G\232\205\343\250\032:\214\246*2\264\302\265\033-D\313Q\025\246\225\246\355\242\220\212JF8\351Q\223\2323HO\245!\367\244\335\216\224\231&\224\n\225EL\2434\374`S\212\344\212\261\032\020\325m\026\247AV#Z\267\022\325\270\305\\\214U\230\305X^\005J\225:T\303\255H\2652\216jq\302\324}MJ\253S)\300\250g\014\340\366\025\223pB\344f\252\002]\272\325\350\227\000WHG\025\024\230\000\326e\311\347\212\244\307\255V\301\335H\354sP\2775\013-W\2213T\344CU\335@\250\231sQ2R\010\271\247\210}i\255\026\332P)\032<\212\201\243\250Y*\026LTl*&\025\013\naZc-0\256)\244SH\244\305!Zn\332n(\013\232k!\246\343\024\n\225EH\005<S\327\222*\310\2178#\265N\203\326\254F\265a\026\254\242\325\230\305Z\217\265Z\216\254\245N\265:\n\262\202\237\212\221j\302\017ZWn\302\221j`jU\246J\245\201\305a\336.\3065\005\270\346\257)\342\272\031$\n*\224\323\0228\2522\261&\252\266i\270\3438\250[$\323J\323\031*\007J\257$UY\340\315@\320\221M\020\372\323\274\254R\371u\033\246j\002\204\032Q\356)\0320j\254\221\324\014\276\265\013-D\313Q\025\250\330S\010\246\021L+M+I\262\215\264\302(\t\232x\216\202\242\243(\r7e=V\245\013\305\033i\350\2705r>EL\253V#OJ\267\032T\352\270\251\343\025j5\253(\270\253\010*\302\n\262\203\212\235E>\236\202\246\007\002\230[\232z\232\2206)\301\352@r+\013Q\220\0075\005\261\310\315\\\007\212\336\237\322\2522\325y\022\230\2109\334)%A\216*\261\213\322\233\345S\035p*\253\014\232c&j\026J\211\343\250\374\272B\224\335\264\326Z\201\326\230\026\215\265\014\261\367\252\256\225\013%B\311Q\262\324,\264\302\264\302\264\302\264\233h\333F\312iNiBb\224\214t\250\230\032m.)B\324\200R\201O\013\212\2263\203W\023\006\254 \003\025e\0179\253\t\311\253Q\245YE\253\n*t\031\353VQj\302\216*@qN\0075\"\323\213qH\274\365\251\001\002\223\314\317JUl\232\260[j\023\355\\\276\2416\351\210\007\275X\265\030QVI\256\226X\362*\233\251\007\245!\213\"\242h\366\324.3Q\354\246\262\325y\026\2520\301\246\221Q2\323\n\361L\333M+M)Q\262\324.\225\036\314Q\266\221\223\"\251\310\2305\003\255B\313P\262\346\242e\246\025\310\250\312`\322\024\246\354\240%.\332M\200R\025\244+M+\232aJM\264\340\264\354R\201O\307\024\016\r[\205\262*\302\236j\324F\256\306*\334hx\315[\215*\302GV\021*T\03056q@\346\245Z~\354R\016z\322\226\240\232E5*rE\027\223yp\266=+\224i\014\267\034\372\326\274<(\251\031\253\257a\232\205\343\250\031j\027\031\250\nS\n\324l*\254\203\255Ve\346\230V\243e\246\021L#\024\230\244+M)Q\262Te)\214\265\031\340\3242\307\273\232\252\311\216\265\013GP\230\352&J\214\2450\255&\312B\224\233x\246\355\305&\332]\224\326\025\031\\\323H\244\305(\024\270\247*\324\230\243fj\304K\267\255Y\215sV\342N\225v1W\"<\n\270\235\252\302\324\302\235N\025\"\324\231\240ry\247\026\002\214\323I\315*\361R\306~a\212\253\253I\266\023\315s\226\243t\244\326\322\034\n\031\253\271+\201P\275Ts\203Q\036i\254\264\302\271\025]\324\203P:\346\2532b\242e\305F\302\243\333C-G\266\223h&\224\255FV\230\313Q2\324\0169\246{\032\202T\364\252\344Tl\242\243d\250\231*\026Z6\323J\321\266\232V\233\212R8\246l\3157m\006<\212h\212\235\345P\"\346\236#\245\331\212U\0375H\300\361\212\265\000\343\232\271\030\253iVc\253q\0360j\322\232\225i\364\340i\341\251\331\244-\212\001\247\nu(\251c\342\262\365w\314x\254\333(\361\315h\203\201Lf\257Aq\305Wq\305A\345n&\230\320\343\221Q\225\246\025\250\035*\006^j\007LT\014\242\242d\315&\312aZ\215\226\233\266\220\212a\024\3223Q8\305@W&\230\313Le\014*\007\212\240d\305D\313Q:\324%h\333HV\200\264\2333L1\322l\243e\'\227HR\224%8&i\342:_.\232\313I\262\236\007\255X\217\002\254F\325j3\232\267\035Z\217\203VS\232\235)\304\320*A\357J[\322\223\2558\nz\323\361\305\000T\313\302\223X\232\223y\222\005\024\220\307\261EHMB\355^\222\303 \324\016\264\213\036\325\317sQ\225\354j\273\256\323Q\232\211\305Us\203P\310\300\212\254\335i\204Rb\230\304S\010\244\333M S\010\024\302\265\033\246EWd\305F\302\243#m5\230b\242!Z\242x\275*\273\307\212\204\2574\233h\333M\305.)\n\342\232\027=\251\3333I\262\223\313\346\202\224\233qO\002\237\266\230R\220\2554\014\032\231*e\340\325\310NqWc\251\322\255F*\302\234\n\\\346\234\242\237\264\321\212p\024\340)\302\236:S\221w\032m\324\302\030\311?\205c\'\357d.js\300\250\231\252\006j\364\362)\247\2574\322sQ8\035\252\274\303\"\253g\326\230\334\325i\227\212\250T\346\230V\230V\232EF\313I\212c\036\324\303HE4\212i\034T,\274\324\016\274\324L\271\025\023-DS\024\231\301\250\244\346\253\224\346\223m!Zi\024\230\247\000\017ZpP)\017\024\302=)\240\034\323\266f\217.\224/\265=V\227m1\226\232#\334x\253P\301\201\310\251\032\014\364\247F\214\235E[\211\263Z\021/\002\254(\305<t\247\n\225EH)qF)@\247\201O\305=\230D\204\232\302\274\2717\022\355\007\212\2225\n\242\2075]\315B\306\275T\216*2)\206\243j\211\207\025M\324\202qQ1\250\233\232\201\222\243e\250\230TD\021M\357JG\025\004\213H\007\255!\244#\212\211\2526\250\335r3P\225\305D\302\242a\305D\303\025\023S\010\244\305\005j2\271\246\225\244\305\030\315\000{\323\200\035\350\300\317\025\"\256i\305@\246\021@\024\255\300\250\031\262j\325\274|d\325\221\307J\221\005X\010\033\265H\220\000sV\221p*e\247\201N\002\244SR\212Z\\S\200\251\000\247\242\3675\233\251\334\355R\252k2\3352w\032\267\234\n\215\332\253\273T,\325\353#\2450\212i\024\302\264\307N*\253\2575ZT\364\250\010\2460\250\234T,*&\025\t\353G4\204f\233\214Rb\2028\250\312\372\324r(=*,qP\270\250\030TL*\031\005E\212M\264\233qA\034SqMaM\305\0053\322\233\267i\245#\212j\360j`h\316iqN\013Mu\250\322\034\265]Q\265p)\300T\350*d\030\251\324\324\252ju5 \247\001N\3058\034S\201\247\2575*\255H\005\023H\"\214\375+\234\271\223\316\227\216\225$ch\247\026\250]\252\006j\205\215z\332\236)M6\232N)\255\315W\221j\254\225Y\226\243aP\275Dx\353Q\265@\303\006\233\232v)\204sJ\005\014\274S\nqQ0\305D\303\212\256\342\241aL)P\262sQ\262S6Rm\244\"\233\212B\264\335\224m\244+\232n)\n\322\212u:\236\2640\317JX\306*QR*\324\350\206\254(\002\244\003=*E\025*\324\313R\001K\216)1\353R \251\2213S\204\307Z\216i\204JI\254K\313\343&@<UhT\236OZ\261\322\243f\250\035\252\026j\211\232\275q\rJ\005.\332\215\226\231\266\241\220u\252R\214\032\200\232\211\352\023P\311Q\324L)\201y\2511\3054\255 \247\036E4\257\034T2-@\313P\262\324,\264\312C\036\352\211\243\250\312\323\n\324dSh\244\315\035i\n\321\262\224GA\217\035\250\331F\332P(\247(\251TT\3121Rg\0254{Oz\235F:sR\nz\232\225\016jQ\322\2274\001\223S \253\000\205\0243\340Vm\365\332\200T\236k\025A\221\363\3335mF\000\241\215B\306\240cQ1\250\311\257\\\214\325\210\316MH\303\002\243\316i\255PH*\234\313UY9\250\331j\006\025\003\365\250\311\301\244\"\223\024\206\222\216\264\230\245\003\212\212E\250\035j\026\025\023\n\211\222\223\030\240`\365\2441\203Q4 \364\250\0319\3057\312\246\262\342\233\2126\322\355\247\010\351\341qHE7m&\332M\264\005\247\205\247\201R\n\220sO\013\351R)+R+\023S)\251R\246\335\201M\315H\206\247CN.;\232\257qp\241\t\006\271\371\244i\245\340\325\230c\332*S\305F\306\240sP\261\250\231\2522\325\353\360\363\305]\2120\0074\222\361\322\242Z\030qP8\252\322\214\325r\265\033-@\351\212\256\351U\331i\240Rb\233E(\024m4\355\230\025\033\255B\313\232\211\222\242d\250\312S\nSq\212i\024\303P\260\346\220-!\2174\323\035\001)\302:v\312B\270\246\342\215\264m\244+@ZP\264\340)\300T\212*QO\247\255H\225:\320O4\242\246Q\305H[\002\253\317\'\312H5\213$\256\354FN*h#\000s\326\254\343\003\212\215\315@\355U\335\352\027\222\241/\232aj\366H\276Y1ZJ\006*)y\250\361KP\270\252\356\271\250\031j2*\'L\324\016\225\003G\232\214\307Ld\244X\211\243\312\346\234#\247yt\245*\'N\265\\\2550\250\2462T,\225\031ZaJiJc%D\311H\026\224\241\246\355\366\245\tK\262\223\030\2460\3153m\006\234\007\024\005\311\245+F(\305(\024\361N\025 \247\255J\275jaJ94\354\201N\rMy1\221U\244\311\3435\003\332\356\031\217\357TA\214M\265\3705:\311\221H\304T.F9\250X\002\274TE\021\201\301\250\377\000v\271\365\246*&I\'5\354\261\304K\364\253d\020\265\016\t4\270\244\"\243~\225]\373\324,)\204sLe\250\035j\"\224yY\246\230E\'\226\000\243\313\024\276]\036]F\313\351Q2u\315@\311L\331M)Q2TL\224\302\224\323\0354\2450\305\232o\225K\345\323Lt\335\224\036\005BFi1\212i\031\246\260\245\035)\300`PM&(\240u\247S\305H\265\"\324\240T\203\245&H4\240\344\323\267b\240\335\311\240\234\322+\355<Uk\245\017\363\n\254\222\02585`>i\216\001\006\240u5\001R3Q\0255\031\316+\337\002*\364\246Hs\322\242\034Rri1Q\275Uzn3Me\246\021\232\214\246i\246*\004t\326J\217fivb\233\232v3\322\243(ED\310MD\361b\230c\366\2464u\023%FR\231\345\321\345\346\232c\246\024\305\n\240\236i\216\203\265DEFE0\256i\245qM+\353Q\2650\323\227\2458\n1HE6\212p\247\212\221jE\251\226\226\202h\034\323&|\016*\020\334R\202i\245\251\205\263PN\230]\303\2556)2*\\\344TmP\265B\302\230\302\275\325\230\223M\'\327\2553u&i\t\357P\271\315Wzh\353JFi\204S\225)JRy^\324\306\212\231\345b\230c4\301nX\346\245\021\000\005\006,\324f\034v\250Z,\236\225\033G\212\211\222\242x\352\023\035/\226)\245)\214\270\250\034c\245F\006)\255Q\225\3154\256)\207\330S\010\250\332\242aM\"\225y\247\n\\R\021Q\232QJ:\323\305J\274\324\202\236\016)wQ\234\323\272\n\212R\270\371\215D\030R\027\250\313f\231\234\032\224\'\232\244\032\240\352`\223\025:\234\214\3225D\303\232\211\252&\036\225\356\254\270\250\3150\232@i\030\346\2425\023\nh\034\323\261I\212\231Tb\236\020\032R\243\025\013\255Bh\t\232]\270\024\320\271\346\235\266\215\240\324m\030\025\033C\270dUW\214\203\322\242)\236\325\031\216\233\266\243a\315E%@\313Q\225\246\354\246\025\364\2462\201Q\265D\334\324dS\010\246\021M\007\006\244\352(\315\004\323\0174\200S\200\245\251\024\342\245\rN\315&\352p4\342\340\n\241p\344\26501\247\346\223v)\240\345\252h\337k\324\027\250X\356\250\341l\255Hj&\250\230TF\275\325\352\006\250\330\322f\2239\246\236\365\031\244\305;\024b\236)\340\322\265F\336\365\036\334\322\343\003\212M\271\247\005\243m\001)\031j\"\010\250]7v\2506\020\324\311#\347\212\205\223h5Y\205FV\243+M)L`1\357P\267\035*\027\250\2150\212i\\Tl2j2\264\303\301\245V\305;9\351Hi(\006\2274\264\345\247\212v\352\\\322\346\243w\305T\221\262\324\253N\3155\215,JpX\323\315%\313~\344\343\232\243l\374\220j\331\351Q\265D\325\021\025\356%\270\250\332\2424\224\235(\3051\205 \024\360(\333K\266\234\0058\014\324n\231\245\013\305\0333K\263\002\215\264m\315;g\024\306\002\242+Q\262\324,*6\030\252\262\014\324\016\270\250\312S\010\002\243`*\027\305@\325\013\323\n\036\246\223\201Q\261\346\2439\246\032\214\212f9\247)\245\315!\244\024\242\224\361NZ\226\212L\320\0335\023\344\344\325|d\323\205:\232\3252\377\000\253\000Si]wDEe\257\311.*\3709ZcTmQ5{fsHi\270\246\221\212JP)\n\322m\247\001N\002\214S\202\323\266\342\223ni\3018\244\333\212B(\3058-\0140*\007\025\031\366\250\230\361Q1\250\217\275B\342\253\260\346\243aQ7\025\003\232\205\215Dj&\034\324m\222i\245x\246b\230E1\205FE4\255&1A\244\315\002\234\264\032U\342\245\007\212\t\246\232\024\363N\342\253\310\230<S@\247\001H\302\244\204\344m4\2450i[\345^k\036f\377\000H8\365\253\310~QCTL*&\025\355C\255:\214SXS@\247\205\245\331K\262\223m.\3326\323\302\320iB\323\261HE4\255\001i\341x\246\270\252\356*\006\030\250\232\242aQ\270\342\240l\324D\376\025\023\232\201\315@\365\023\n\214\217Jcq\365\250\210\3151\251\270\342\232\325\013\032a4\334\321M<Sz\236(\247\n\\\361H\r<\032\\\321J:\322\323Xn\250\312\021H)q\3054\214\034\212x\224\367\243%\301\315d\314\000\270\343\326\264#\037(\241\205D\325\033W\264\201N\305(\024\025\315\001)\352\224\375\264\273i6sMd\247\204\244aH\251K\214R\342\220\255&(\003\232\220\014\212\215\305WaQ0\315W\224\343\201U\036LTfBi\205\261\326\243b\010\340\324\'\223P\311\317J\200\323\010\3151\205D\302\230ED\302\232x\250\336\241j\214\322\037j94\323B\014\320\303\024\320i\335\251)\300\323\201\247\nu)\034Rc\024\244pj\035\264\264\021M#\024\341\367X\326C\374\323\3765\245\030\371E\rQ5D\302\275\245\016E<\014\322\201N\0034\360\264\365\030\024\354f\200\264b\215\242\215\264\2052i\312\264\215\035.\312M\224\233(\330\007Zq\034qP\270\250\030T\022p*\204\362c\201T]\2114)\315+.F* \204\032G\025]\315@y8\244n\005Bi\246\243aQ\265D\306\241sP\261\250\311\246\203\223Rf\232}\350\r\203C\220E0S\373Sh\025\"\212x\247\001N\002\227\031\243\030\250X\037JA\326\235\212i\024\262|\2201\365\254t\033\246\255$\351CTL*&\257dS\201S!\315J\0059E<\nv3N\002\227\024\241h\333H\306\221Fj@\264\355\264yt\205qH\026\215\264\204Uw\025\021\\u\252s\347\234Vl\334\036j\253\002\335(D \324\342>3Lu\364\252\262\034\032\204\340\217z\201\206\323Mc\221P\2650\232c\034T\016\325\0135B\315Q1\250\3157q\355N\317\255\004\323sN&\201KE\003\255J)\302\244\002\224S\300\245\307\265\0057\014Ur\273X\212\\R\005\311\250\357\316\310p+2\325r\371\255 8\246\260\250\232\241j\366E\351O^\rZ^E<\n~)@\247\001N\240\361I\270\na\303\037JT\0252\212\220\npZ\ng\265FS\024\230\2467\025\013\016\365\003\325g@A&\251=\276\366\366\250\336\334/j\213\313\013A \n\205\330b\263\356\034\202qU\274\323Am\335j6\250X\323\013TL\365\0035D\306\242cQ1\246\026\246\347\232]\324\205\361M\337OBZ\244\351M\007\232x\024\240sR\250\315;\030\247\212p\024\360)\330\245\002\253\310\270z1J\253\315S\324\217\312\005U\264\\\n\275\216)\214*&\025\013\n\366E\034S\300\253\021\2361S\250\251\002\322\355\245\305\007\000f\243-H9\316i1R\240\251\007\024\340j\302-+\n\214\212\215\252&\300\250\034\325g\3115\003\324y\002\241\221\252\254\215P\263T\016\325J~k>F\330jDp@\2476*\007aP\267\265Ws\212\205\232\242g\250\232J\210\276i\205\251\241\251\333\2513M&\237\033T\244\346\205\034\325\2208\243mH\243\212v)TT\200S\300\245\305(\246H\2319\246\355\247\204\302\223X\367\255\276B\007J\222\336<(\253\030\250\336\241j\205\253\331\224qN^\265\"\037\232\256 \310\251\000\245\305(\025\034\265\0363O\2152y\247\024\000\346\234\007\034R\323\324U\244\340R\265B\325\003\236i\2147\003UO\007\223Q;dUv\340UWl\032\205\3375Y\316MD\306\240sUf=k.v\353Kn\341\207\275H\357\212\201\244\006\242g\250Y\263Le\315@\353\212\254\324\302p*2\324\335\324\340\324\271\246\026\247\304jqR\240\346\247\307Jx\024\360\264\355\271\247\205\245\003\024\361N\305.)6\344s@L\221Q]\270\2110:\326:\251\222L\232\272\213\201Jj&\250Z\241j\366\304L\212f9\251PU\250\316\005;94\345\346\236\005G\'&\221W\234T\244`qM\344\322\343\002\224\n\224\n\231\017\024\023Q9\250\017\255\034\021UYpOz\202CU\344\351Tf<\361P1\250\034\324D\343\255F\304\032\2550\3105\225t\207\223T\321\312\036*W\230\260\346\241\337\223C\270\307Z\256\317\317\024\3174\212C.j\0269\246\021\232\211\226\242<P\032\227u%I\030\346\254\245Y\215sVB\212P1N\024\341N\247\001O\002\235\2121J\0274\343\210\324\223X\367sy\256@\242\030\260*|b\230\325\013T-P\265{x_\226\205\2175 \\S\306E(52\260\002\227w\2457\2559\0079\247\023\305 \024u4\341\326\245\002\236:R\023P\261\315D\306\243g\307\322\242f\316qUdj\253#\325Y\rWcP9\305W\221\352\271\222\243w8\252S8#\004U1\215\324\222\201\216*\253\270CP4\271\351Q\2310i\215&i\236`\246\2313H_\024\201\367\036h\221x\315@z\323\205(\025i\024\005\367\253\021DO5r8\3609\025&1J\0058\014\323\200\245\002\244\024\352SH9\251\020`\363U/\246\302\355\006\263\242\217sd\325\300\270\024\326\250\332\241j\205\252&\257s\000m\241F\r<\n\\R\342\212QR\216\224\341\305\035M\006\222\236\242\244\024\244\340TL\325\016\356i\214j23PH\330\315T\221\352\254\215P9\250d8\252r\311T\244z\256\322s\305!\220\221\315V\227-U\330\005>\365\013\270\317&\252\315\317CU\217\006\242rsQ\226\3050\265\001\270\246;\322)=ju\223\214\032M\231\351J\027\024\340\274\325\210\306p+B \025j]\330\245\0074\372x\036\224\360\236\264\273pi@\245\244\357\305H\243\271\246\314\373W5\225!2\275O\034{E<\361Q\265D\306\241j\205\252&\257vQ\223O\013\315(\024\340\264\273h\333F\332p\024u\247 \240\365\240S\324S\307\002\230\315P;\324\005\271\241\337\025\031\224b\252\310\371\252\222>*\263\275WiqP\311(#\025FBI8\252\333Y\316)\031\002\216j=\303\322\242\221\205P\231\262p*\264\2101\235\334\325f8\353P\273T,\325\003\311Q\031i<\332\024\356<\324\243\212w=\251\351&\323\203S\207SN,;T\22075y\rH\005J\202\245U\311\346\247\003h\340Uw\224\253\363OY\225\273\323\203\002i\304\201M\363\000\245\r\232\212y21U\343\217\234\232\233\2651\215D\306\242cQ5B\325\023\032\367\244\\S\300\247\005\247\001K\212\\{Q\212gCG&\2348\315%8S\305)<T\016\325]\332\242\335\315A4\265\030|\212kt\252S63T&\227\025L\313\270\343\232^\325\023\020*\274\222\355\351\305Uy3\336\243/\357U\246\233\035\rg\315+)\252\257p\335\352\006\231\215F\\\232\211\336\240\221\352\003%&\363\232\225\036\246\017N\363\005*\266ML\246\245U\315X\210m5q*\302\363S%M\030\313T\300\214\32471\003\326\241\020\205\357FpqR/4\2059\241\334(\250F\\\344\323\372R\023Q\261\250\230\324Lj65\013\032\211\215{\341\247\255I\326\224\nu\024S\010\364\243\030\244\242\234)\340\342\243v\252\3625Wf\250\367`\325y~f\300\245\013\264TR=Q\235\305f\316s\234Ud\030j{6\005V\226N\0175FY@\357U^\340\n\256\367G\267\025ZI\262\0175I\3459\344\346\240\222L\324-!\002\231\347v\246<\234\324\022=W\337\315.\372\2266\315I\273\024\34595f4\316*`1S%L\255V\342l\212\267\030\251\2623R\306y\025ch\340\367\250\2475\016Gz\000\035i\305\325\005@\323\0268Z6\347\2559@\024\214j2\324\3065\023\032\211\215F\315Q1\250\230\327\320\000d\323\302\323\261N\024\242\227\024\224\323I\236)(\245\024\244\340T\016\365Y\336\241f\2463\0002i\261\214\363D\216\000\252\023\313\214\325\007\223uU\221\211\310\2507\001\326\241\232a\214\n\245$\243\234\232\314\232}\316pj\007\220\001\315U\226\340\016\365N[\237z\254\3679\357P\264\376\365\023OL\363rhy\006*\006\2235\021|\032O2\245\212Nj\312\275M\031\031\311\253j\343\034S\303T\252\365*\022\306\264aP\024f\254\007\300\342\234\246\254Fy\025h\0163\351U&\223\3465Y\244\'\245 v\247\034\221\315:5\301\251\030\342\231\272\232\315Q\026\250\331\252&j\215\232\243f\250\330\324lk\350UZ\220\014S\261N\013F\3321HE4\212\214\360h4\235)sLv\252\316\365]\332\242\3150\234\237j\014\201G\025Zy\2532yI\357U\267\032\206I\007j\252\357\305R\232oJ\245+\2229\252\022\270^\365Fk\214w\2522\334\023\336\252<\331\357Q\0313\336\230\362c\275E\277\336\2173\025\033L}i\206Zi\2234\201\351\351&\rYI\211\253Q\271\"\247V5\"\311S\243\325\313c\226\346\264Q\270\247\006\346\245SV\021\352g\230\210\316*\233\266T\223L\006\236)I\245V\346\225\332\230[\212\215\232\243-Q\263Tl\325\031j\215\232\243&\243&\276\214Q\305<{\323\300\247Rb\220\323I\246\226\250\330\212nsE5\233\025\004\217U\235\352\006z\215\237\002\253\313)\003\212\254\327\030\030&\253\3119#\255Ty\200\252\317?\275Wyrj\255\304\373W\255Pi\300\0075F\342\357\260\254\331\256I\357T%\236\252I-Wi)\003\323\035\351\201\263J[\212\201\232\230Z\233\270\323\303f\244S\315Z\216\255FqSn\251\020\346\254\306j\334\'\006\257\243qO\rR\253\324\310\365&\340\303\031\252\316\330R)\241\251\341\250\337J\036\202\364\322\374Tl\325\031jc5F\315Q\226\246\026\250\313S\t\257\243\326\236)\340\323\251\245\2050\2650\275F^\232Z\231\277\024\031*&\222\240w\252\356\365\003\275A$\234UYd\343\255Uy=\352\264\217U\244z\253$\225\001\223\255P\272\233<\n\241#\026\035j\224\307\025Bf\252R5Vv\250\031\351\276e4\276h\337\212i\222\230\315Q\026\2405=ML\255\212\261\033\325\204z\224=L\215Vcj\265\024\234\325\324\223\212\224=H\257\232\225_\024\342\374\361QJ\334\373SCS\203\321\276\224=\033\351\245\3522\364\302\324\302\325\031jajajaji5\364\222\323\3058PZ\243-Q\263Tl\365\031zizc5Fd\250\332J\205\344\250\035\352\274\222Ug\222\252\311-Uy*\254\222\325i%\367\252\317&j\031$\n+:\342A\234\3257\237\216*\234\262\016rk>iG5M\345\252\362IU\313\022h\372\232ajM\324\233\251\214\365\031z@\365*=N\255\221S#b\246V\251C\324\321\275[\216LU\204oJ\265\034\270\340\325\205|\364\251\221\252`\324\245\275)\254r\rB\037\236i\333\3517\322\357\243}4\2750\2750\2750\2750\2654\2650\2654\232Bk\351Q\322\234\r\005\261L-Q\263\324L\365\023=0\2754\275F\317Q3\324FJ\205\244\250\036J\255$\225VI*\254\222UY%\252\222IU\236Z\201\3445VY}\3536\346\343\255gIpMWyI\252\322d\365\252\316\r@\355P\263Te\375i\246Ja\222\220\313M2S\013P\rJ\206\254#T\352\325\"\265H\032\246\215\215[\210\346\255#b\247V\315X\205\271\253j\325 z\013\322o\252\356\330\177\255.\372B\364o\243}!zizazizajijijM\324n\257\245\305\005\261L/Q\263\324L\365\033=F\317Q\263\324fJ\211\244\250ZJ\205\244\347\255D\362T\017-V\222Z\251,\265U\345\252\262KUd\222\253\274\200\014\223Ud\234\023\305S\232oz\316\232\\\346\2523\n\211\210\250\235\352\254\222Uv|\324\016\325\0135FZ\230\315Q\226\305&\352P\325*\232\225jT\253\010jU\251\026\247\216\256F8\251U\252d&\254\306pj\312\275?}\033\351\013\324r\034\256Gj`~)\013\321\276\215\364\322\364\322\364\205\351\273\251\013SKSwQ\272\215\325\364\301jc=F\317Q\263\324L\365\033=B\322Tl\365\031z\211\236\241i*\273\311P\264\265]\345\252\322IU]\362j\274\257\201\326\251\264\234\362j\264\327\n\265\237,\345\317Z\200\275T\232J\243,\225X\311\232cIP\274\225RW\252\306L\032k>j&j\2179\240\324mM\247\255L\242\245Z\225\005N\242\245Z\225jd\253(ML\265e\rJ\255S+\361N\337N\r\232B\324\335\334\021P\206\244-I\276\202\364\322\364\205\351\273\350\335I\272\232Z\223u\031\245\335_K\026\250\231\3522\364\306z\211\236\241g\250\231\352&\222\243/\232\215\232\253\311&*\264\222\372T,\304\324.\330\352j\264\222UY$\252S\314{U\t\'#\275S\222]\307\255B[\025\023\311T\245\222\251H\3715\0136*\026z\201\344\252\362>j\271=i\205\251\245\251\231\2434\207\232LS\324\032\231EN\253S\"\342\246Q\232\225V\245U\251PU\2055*\361R\251\251CT\240\361K\272\234\255C5F[\025\036\356i\013SKSKSKRn\244\335K\272\215\324\233\2517Q\272\224\032\372I\244\250Y\351\205\3526z\215\236\241g\250Y\3522\371\244\006\230\355\201T\245~\265X\266M#\266\005S\226J\254\362U9&\252\222\310\rg\316\370\'\025I\236\230d\250\235\370\252R\234\232\256\347\025]\332\253\273T\016\325\0136j75\0214\204\322P\005.)\300T\252\265*\255L\213S\250\251UjU\024\354T\211S\251\251\224\324\253R(\346\245\307JN\224\3654\255P\267z\2046sHZ\232Z\230Z\232Z\220\265&\352]\324\273\2517Q\272\215\324\273\253\350\306\222\243/L/Q\264\225\013IQ\0311Q4\231\250\313\322\254\237-C$\231=j\274\244\036I\250I\013\316j\264\323g\201T\235\352\254\322\340U\007\233\223U\244\2275VV\3105U\372T\r\221P\263T\016\325ZCU\334\325w5Y\330\212\2179\2460\3154\203M\305\030\240\n\220-=TT\252\242\245U\251\225jUZ\224\nx\025 \025\"\214T\200T\212je9\251\343\\\323\363\203M&\234\247\326\206jc\237\2275S8&\202i\244\323\030\323\t\246\356\243u.\3527Q\272\215\324n\240\267j\372)\236\242g\250\332J\211\244\250ZJ\215\244\250\332J\214\2754\311\201PI)\035*\264\223\037Z\257$\307\326\253\264\231\315@\317U&~\rf\313\'5Y\346\250\232L\324L\325\023\236*\253\266*\027j\256\346\2539\252\356j\026\250\310\240\323M7\024\240S\202\324\212\265 ZxZ\221EL\202\245\013R\001R\240\251\002\324\212\264\360\264\340*D\253qc\024\215L\3174f\232\317H\355\362UV<\320M4\232a4\302i\231\2434f\227u\033\250\335F\352@\325\364+IQ\264\225\023IP\264\225\013IQ\264\225\031\222\232e\250\314\231\250\244z\251$\225Y\244\311\346\241y*\273\313U.%\342\263e\222\2533Te\361M\3633M/U\344\3475Y\332\241v\252\362\032\256\325\013u\246\363IF)6\323\302\323\302\324\201i\341j@\265 Zz\214T\312je\\\364\251Uj@)\340b\236)\330\245Z\261\031\342\203M4\225\024\234S\031\262\225\001jM\324\231\246\223Q\261\246\223M\315\031\243u\033\250\3154\265(j\372\t\336\241i*\026\222\241i*\026\222\2432S\014\231\246\263SK\340T\022IU\036Nj\273\311\212\255$\265U\346\367\252\223M\232\252\3075\013SJdu\250_\345\342\230Z\243v\342\252\310j\026\250Z\240j\214\2126\322m\245\013F\312P\265\"\255H\253R\005\251\024S\361J\005H\253S\3061S\201R\001K\212z\2558-(\034\324\212\010\247S\t\246\026\244?0\250\266\225\353\322\241\221q\310\250\263\212\t\244&\243c\232i4\322i3Fh\335Mg\244\006\234\r{\353\311U\236J\205\244\250ZJ\211\244\250\232Za\232\217<t\250\344\233\336\252I8\035\352\253\334\201P4\331\025ZIj\2735B\347\212\200\232\214\232ij\206S\232\203&\241\221\252\271l\324lj&5\031\024l\365\245\331M+@\024\354R\205\251\002\323\302\201O\013R\005\247\205\247\005\251\225*U\025*\212\220\npZp\030\247\212r\217Zp\024\204`\323\030TMQ\346\235\234\216i\204\216\207\245W\2210x\250\263HM4\232a\246\223HN)\245\251\013SsK\234S\201\257vy*\273\311U\336J\205\244\250\332J\205\244\250\232\\T^v\016MC-\317\275Sy\211\250\231\352?7\007\025\024\217P4\224\302\331\250\211\2461\246\026\250\\\324]MG\"\212\200\212\214\255D\302\230E\030\245\240\212LR\342\224\nz\323\305=EH\005H\005<\n\225je\247\250\251qJ)\300S\200\251\002\344\322\355\305\014*6\024\302\265\023G\3157n)\244S\010\310\301\252\262\251\006\230\016i\r0\323I\246\023L-M\315.isFk\334\032J\256\362Uw\222\241i*\026\222\242ij\027\226\253<\276\365\003I\357Q3\324O.\005W2\363N\336\n\324\016y\246\027\3057~i\214\325\0314\306\351Q7\265D\315Q\261\250\311\24674\314R\355\243\030\240\322\036\264b\236\0058.*e\214\021\301\247l\3059EH8\247T\213R-J\246\245^i\340R\212x\251T\323\210\310\244\333M+L\"\243ja\250\315F\302\242q\221UXm4\302i\244\324l\330\250\213f\233K\2323FiA\257gy*\007\222\240y*\006z\211\244\250\036J\201\344\250\032J\201\344\250^Z\201\344\250\014\234\322\211\251\305\363Q\26574\023Q\223L\3150\324,9\246\021\212\214\323\0174\336\224\271\244\315\035iqF\332z\247\275L\027\326\2342:T\241r9\243m8\nZx\251\026\244\025*\032\230\036)qN\035j@02ju\301^(8\250\310\024\306\025\003\212c\n\214\323\032\2435Ve\346\253\223\212\215\237\322\2429&\222\2234\231\245\315.h\025\353\355%@\357P<\225\013IP\264\225\013\311U\336J\201\344\252\357%B\317Q3\324e\251\233\271\247\2074\355\371\024\231\346\220\2650\232a<\322\036\224\316\325\013\232\214\232ni\t\246\021F)\312)I Rn\346\236\257R+\212\220\032z\276)\371\356)\300\323\251E<qO\006\244V\251\320\346\245\307\024\243\255K!\004\014T\221\034\nV\250\233\212\215\215Fy\2465D\324\303Q\266{Uv;\270=j\264\203\025\001\246\023\212i4\334\322f\212\\\323\201\257\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\006\nIDATx^\355\334\331\222\343*\014\000\320\251\374\377/_\327\255^&\023\307\361\0166H\347<ug\263A\002\204\235\356?\177\026=\336\037\000\310\3439\005\016\257\217\002?\014\214\340z\t\260j\265\222^\022\200Z\376\016\255.\206\330\\\272NO~\372\010|\231\313!\200l\254\224@\010&\263\334\304\037v\351u?|\346\274M\023\000\000O)J\2433\265#\001H\200W\237\306\374\260\322E\237\336C\034\263\361\235}\242\tki\013\034\021w`\265=\2435#n\002$#\337\001\350\325\236b\344\343k-\202\313\336\373\347c\'\002\000\000\320\250\367]\035\000deM\204-\334\003\200\027\351\006D\272\006\357\224\246\226(\222\010\355\366\326\243\335S\273\330\277\216\360\325z\010\312\330f\003i\002!\030\312\225\005\336B\310\035r\3330\0026\274\204\261X]v[kn;0\000\000\000\255^\r\264U\00463a@h\215\226*\244e\321\201>\030\253\237<\327\324x\335s\331\337\005\014\001;\017\000\310\300\325\025\000\2567\267\203\236{\374\220\242\037\226\200\222\000\340\020\313Mr\022\000\240\030[\022\216YX\214\027\236\242\031C\307\203\277\3373\377\344\266\377Bz\327q\271\314\362\\,\001\000\000\202[.\007\211gs\304\177_\270\371\365\304\"\360\331u\232\001\235\2366\274\221\311\300-\032\276\027\320\360\251\305\262m\001\332\366*\256P\374\336q\351\317\003\000\000\240_\007.\000\034x\013\037T\337\236\377=@\365\003\001\000\274R-\002\375\010\271]*9\r\207\354 \000\000\000\366\262=\034i\240;~O\341\3305\200c\357\2421g\322\360\314{i\310\321\261,\001\202\220\000\000\300\026u\326\376\311_;\037-M\306\312|JD\337=3\354\350\237\367\000\365\244\347s\207jv\214\177\000 \026e@^\027\307\376\342\303\261b\020\221\344\\\034\001\312+=\263X\251j\321\263\000\364\241tm\261\327E+\346E\207\341V\367E\371\276#\323\004\t\200$\310N\002\000\020\304\335\027\010\370L\251\321$\303\005\000\000 \205\347\366\317>\020\000\000\346\204\272\231\025\2521\020\302\337QYit>\367\373\225>\237Rf\003t\356\222\315\373\273g\017CtB\017\274/\t\2045\236\362-\000@\313n\232\243n:\354uj-\372\265>\227NH\200N\010\024@\026f|\000\322\010\177%\347\234\331\356Q,\360\211\274\210hv\032\000 \021\253A^\207b?*\n\037J\304\276\274\306k\030~~_K\203b1^;PGv5e\327\213+(\026@\000 55\005p\237\331\031h\366\211\266\335v\332+\007^yz\352\356\355n5a\033\006\020\307\356E\013\000\350\2225\377\220\323\273\332\323\037\320\233(\211V*pM\365\307\025\'s\3051\340v\022=9\t\000\220\315\317\314\177f\376/\265\275\340\026gBO \3061@\034\026w\000:\365\334\226\254\355O\326\326\272\265\347I\351\353\337#\214eK\224I\007\000\220E\266%o\302\032\010o\014\212\344$@.\3432 }QpP\244Q\023\251-i\t\"\000\000k\324\214\'\331=\323\244\303#\373\360\033\001\240\013V:\000R\033\206\351\327\343\263K\333!i\033\236\335k\340%\001\000\000\267R\220f%\362\367\320\357\000\000@L\253\273\035_\364\017\332\003\2555k5\023/\326\332\371p1\t\000\000@o\036\207vy\t+\337\355M>\324\243tO\334\001j3\323\002\r\331\276= $\t@\033\232\250\216\374\337\204x\232H\254\375d\"\000\000\334\257\323\355\004u\004\332\246\311l\370\307x \227@\213\031@%j\203\276X\3316x\351\244x\371]\240E\005>\242q\001[\370lR\300\266\0010\343\247\242Q\374%7\310\201\254*\307]Q\331\262\237\340\307\215Q\334\226\225Ub\0228\362\247\001}\306\247\317\263\256\356H\002\320\200\207\214\246\016\211\005\345Xbc\020\307\344ZK\200\342\347\023|\345\037\312\367X\020o\201\017\330M\337\033\335\200\355\002\200*\202\327\204\300\275\224\345e\350\307\234\252.\321U?\2746#\"\210b\201,\366A\271\350\266\250D\026\000xuam\320\3656\023\000\000B\272pC\000e5\277\305l}t5}~\315G\267\007Y:\261\351L\006\000`\037\305\335\001Y*\177\026\254\3747\260\346s\344\367\364\233?\317\336|\367\353\320x\307\216On\364\333$\255\233n\0104\313\310IN\002\320\271I9@O\204\357rmM\372\022\340IW\314\3201k\226.\366,<\365k\375\025\177.\2306>\035\340\323c_\346\036\347\3338\240\233\302\013\264i\347\000\336\371r\000\200\306\274U3\212\233#\036\017;f\000\216\262\366\002@\233\336\327\350\367\337\273\366\322\230\274\373\331\363-\017\225\023@\n\347g>\"\350~\375\352\276\0017\323\177\000O\243)Q\241\224\211h\247\246\030b\201\364\000\200\313(\312\001\340(\233W\200:\314\257\000\204aQ\333a\347u\312\235/\007\000\332c9\007\2022\275M\244\332\034\213\377~\361\372,^\213\000\000\000\226\330\005\001\000\344\223\352\346\017\000@V\212>\026I\020\000\210\305\332\016\231Mf\200\305/\202,>\t\000\000\000\320\024W2\000\200\350&\367y\200\210\346\206\372\353\343\377\275\374Lvs\031C\022\022\000\000\000 \231\177\267\305\335 \007\000\000\200\256\270\273[N\207}\331\341)\003\000\000\264\302\226*\013\221\006\0023\305\001\300Yi\2777=\214\352\210\264\335\220\272\351\000\000\000t\306=\201\354\356\276\212!\003\201UwOT\000\000+\354k\000\016\262\337\343\213<\000\000\272\246\230\001\000\200\350T\375\200/\005\364\345w\336n1j\377\003\020\243\377\206\213\235b\021\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-7.textpb b/core/res/geoid_height_map_assets/tile-7.textpb
new file mode 100644
index 0000000..83f1fcb
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-7.textpb
@@ -0,0 +1,3 @@
+tile_key: "7"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\364\241\311\247\272\014UR\270j\221E;\024\240R\342\227\024\355\271\244\3074\341\326\234)\340S\302\324\2121O\025\"\324\202\236\265*\324\213R\016\265 \247\221\300\247!\035;\324\312)\342\235\232p4\352\rFx\346\254B\341\206\r<\257j\257  \321\022\0265<\361\205N:\325#\2321IM84\204\n\215\224TD\n\211\361U\334Uw\025\003\212\201\305VqU\334T-P\270\252\322\016*\234\253Ud\025Y\305FV\242\"\241a\232\214\245D\351Q\024\250\235k\327\2323\236)\256\016\334\032\213m8\np\024\340)@\245\3058\nP\264\273i\341i\301i\340S\200\247\201O\024\360*E\251\026\244Z\221jE\251\224\016\365\030 \310qV\024\323\301\247\003N\024\340ii\254)\250v\265[S\221\232c.\346\346\254\300\2358\242\362<(\"\263\366\322m\244+LaL5\033TdT,*\027\250\\Ug\250\030Uy\005We\250XT\016*\264\202\252\312*\234\202\240qP\221Q\262\324L\265\031\024\306\025\013\n\211\205{\010\036\265\024\200T;y\245\013N\333K\266\234\005.)\300S\200\247\005\247\001N\002\224\np\024\360)\300S\305<T\202\236)\336b\257SR\t\227\037/&\201\276C\311\300\251\321v\212\224\032p4\341N\024\341N\024\244f\243#\006\254Dr\274\320\300\203V\255\337\212}\340-\030=\2534\214RQ\212c-F\302\243+Q\270\305@\365]\352\026\250XT\014*\007\025\013-@\353U\344\030\315U\220U9\005VqU\330Tei\2053Q\262\032\210\245D\313\216\265\013\212\205\305z\363q\322\240y0phV\rR\252\322\342\212)\300R\343\322\224\nx\247\001N\305-(\247\np\247\np\247f\202r1\232b\214I\216\306\257\244`\n\225i\340\323\201\247\003O\006\234\r8\032x\247\001HW4\014\257J\221X\265O\037\006\255I\363\304@\254\366\217\232aZLSH\250\312\324o\305Wz\201\352\273\212\210\212c\n\205\326\241+Q2Uy\027\025VAU$\030\315R\222\253=@\302\230E5\216:Tg\232\215\270\252\356j\007\250\034W\255<\200\n\243<\302\2515\367\224\371\315\\\203RW\003\232\224\335\016\306\234\267 \367\245\373H\315J\223\206\251D\202\234\034z\324\313\315;\024\264\003N\247R\212p\245\315;\265F\354W\232\204\334\342A\3175\247\004\333\30758\251\001\245\024\361O\035i\302\236;S\3058S\351\030qO\203\275J\006\rJ\200\223\365\250\245]\246\2414\322)\244Tn*\026\250\034T,*\007\025\t\025\033T-Q3TLj\0075RSTe9\252\216*\273\255DV\232\313Q2Tl0*\273\232\200\324l\265\023\255z4\267\003\007\232\313\270\270\311<\325\tK9\342\230\211*\037\224\232\260\262\314\007SR\255\304\242\247K\306\376!Z6wq\271\001\216+ac\211\324\020\334\3242\304\253\321\251\022M\275\352q>;\346\246W\rN\306zR\342\224\np\247\nQN\002\231*dV;\356[\234\023[V\317\200*\3720\"\244\007\024\361O\024\361\3058\032p4\360i\342\236)q\272\236\213\264\324\246\245\214\322N\205\206@\252\244\021I\214\323J\323\031j\026J\201\326\241e\250\035j\026Z\205\226\241u\252\3561P\265W\220\3259Nj\254\225Y\226\242e\250\310\250\330TNqU\2449\250\030TdS\010\250\334q]K\\\356\025\\\202\346\246H\272T\313\030\251\004c\322\235\345\n_!Oj>\317\267\225\342\246I$A\200\306\247Id\'\3469\0251\215\230d\032\201\244\222>\271\305Y\266\273$\326\224r\206\02584\340i\302\236\0058\nv*)\030(\254\231\030I?\313ZP\214\001V\343r\rZV\315H\0168\247\003N\006\234\r8\032z\232\224\032vjd\0250ZF\351\212lNw\n\270pW\212\253\"\342\241\351IM8\250\236\241l\032\211\222\241d\250\035qP\262\324n\234U9V\2538\252\262UI\005Wu\250Yj\026Z\215\226\253\310j\273\344\324%j6\025\031\024\322*\027\256\221b\247\252\000jA\307Jp85 l\323\301\247n\247\251\315<b\246A\307\322\247\004\355\310\374i\031C\212\245*\030\216S\212\236\326\364\202\003\032\325\216`\303\203S\t*EqR\253\n\220\032R@\352qY\367\327\001\020\205\357Tl\327s\3565\265\020\030\251\024s\305YQ\3005 j~iA\247\003N\006\244SO\006\236\234\232\270\215R\001L~\225[y\215\262zU\350\246WN\265\034\247\232\200\3223\201Q\026\315B\3715\001\342\234\016W\232cTL\265\021Z\205\305T\225j\243\255U\221j\254\213U\331j\026Z\205\226\241qU\235j\026Z\205\226\243aQ\221Q\267\002\253\270\256\246\202h\335F\352p|S\303\323\267\323\325\352Uz\231_<T\253&:R\207\301\241\300qT\336\022\255\225\251\340\270)\303U\305\272\351\315J\267$\036\265f;\241\336\246\373j\201\305V\236\367\336\263\036G\270\177j\321\266\214\240\025\240\215\212\231\033\326\254#v5&y\247\003J\r<\032p4\360i\340\324\261\034\346\255G\324U\2208\246\260\252w*q\305$\014Ur;U\304o1zTrD\303\'\025M\362\247\232\001\310\250\334\324M\355L$\212\214\271\357HZ\243cP\265W\220f\253\272\325Y\022\252\310\225Y\322\253\270\250\030TL\265\003\256*\006Z\205\205D\313L\331P\313\201U\034\327M\272\220\265&\3527\321\276\234\037\232xcN\017R+\324\311%J$\251\267d\0029\247+z\323\260\032\232a\006\221\240*F\r&]i\004\222zT\352]\307Jp\267.~cVa\267\013V\200\333\214S\325\252\302\002}\252\324})\344\323\201\247\212p4\340\324\360\302\235RBy\253\221\236EZV\315;\031\250\244\2175Q\334[\003\221\326\241\267\324B\313\216\325\260\223,\313\322\253\\@95A\270\250\330\324D\340\322\036j6\025\013\002)3Q\2675\013\324\017P8\315U\224b\251\311U\330T,*\026\025\023-B\313P\272f\2421\324R\035\242\250\310rj\006\025\321f\232Z\230^\215\364\273\250\335OY=i\373\307jz\275J\257\212\231d\310\305J\222\021R\254\204\234\324\350\371\025&jD\3062FE&\325\'\216\224\341\020\364\251Q@\343\025&=iA\247\356\243uH\2221 f\257\305#*\362*A \'$\322\203\371R\371\200w\247\007\317\265<\020\335\351\341G\367\215/\314:05$nC|\325z3\337\265ZF\251\224\322\221Y\372\224d\247J\310H\210`En[\202\261\256MYl2\237Z\314\230`\232\254MF\306\224\036)\254*\027\034\032\216\230\325\023{\324\016*\273\325Yj\243\216\265\003\212\205\224\324ej2\265\023\255@\303\025\014\214*\224\315Y\357\'\315Q\264\265\320\026\246\226\250\313P\032\215\364\340\364\340\324\360\324\365z\225Z\245G\305L\255S#\35550q\236*P\334T\261\313\216\243\212\220\225\352*h\361\267=\350i\001n)w\322\356\24058\032r6\r]\023\226@\270\351J\244\324\241\370\245\r\315;u85H\256j@\364\354\325\313yr\274\232\275\033\202*ej\225Z\251j\023\2026\016MS\215G\031\255 \000E\307J3\317\025R\344s\232\244z\324mB\232SP\310j#\322\243j\205\315D\306\242|Ui\024UWJ\256\313Le\250\\b\253\261\305W\222LU)\256\000\351T\244\270&\252\311)j\254\325\023WB\315L-L-I\272\215\324\007\247\207\247\206\247\206\251U\352D|\032\235\037\336\247V\315J\215\371\324\252\365 j\230>\341\322\236\257\371\324\204\250\307\257zqe*1\326\223u8\032pj\221\r^\205\224!\365\251RA\214b\220\236y\2434\240\323\305H\264\360i\340\324\3219C\317J\277\034\230\344T\253p\007=\252\031\365 \200\205\252P\271\231\2131\315]\333\307\025f\026\334\230=\250\'\006\240\270?-S&\243jh84\273\252\'94\302x\250\232\241~*\0065\023\032\211\252\273\340\346\240b\001\250]\200\252\322\275Q\232`\277Z\317\226Fj\254\365\003\212\205\205B\302\241j\333-L-L-M\335F\352]\324\340\325\"\265<5H\034c\336\236\216*Tz\260\262qS\307&\322\rK\346\2069\003\025 \223 T\310\343\036\364\360\370\346\237\36374\241\251\301\251\301\251\352j@jdsVT\232\221NMHq\332\200*@)\343\232P)\375*Tn0E=da\307QJ]\210;\016\rUh\345rwQ\004\255\013\355qZ\221I\270U\210\376\\\232Rj)\233(j\236)\255Q\221\315!\351Q=B\315\212\214\277\255F\346\252\274\233MFe\035\352\026\225}j\027\220\016\365VI@\351U$\230\325Ief\252\257\317Z\205\305@\342\240qP\260\250\036\241j\325-L-L-M-@jp4\006\251U\251\341\251\301\251\352\3252\265J\257S\253\344T\350\330\025*H\000 \363OW\251\225\263O\363\010\030\007\212\221Yvd\236i\003\023\323\245=^\244V\251T\324\311\326\256G\265\207&\226\236\rJ\244S\201\251\001\247\216\264\341\315=jE\247m\311\310\353OV*p\303\"\244\222\3329Wp\024\220\257\226p{U\235\324\205\252\263\271v\300\351JS\002\242aQ54\216*7\351U\332\241`s\315B\344\363\203U_\336\240r\rB@\'\236*\tT\212\254\365]\352\007\252\357P\260\250\034T\017P=B\325\003\n\320-L-M-L-@4\340\324\340i\352j@iwS\203T\252\365\"\265X\215\252\300$u\251\021\206EX\334:c\232Uz\2205.MM\023\200\016ic \223\272\245FS\327\212\224\020:\034\324\310julT\310sR\212z\340S\367\np>\224\360\364\360\324\365j\2205L\2074\346 \nXe\031\332MJ\024~4\355\330\246\034\277\002\200\241hcU\330\344\323\010\250\311\250\230\324-\315D\375*\273Uy\006j\263\214T\017\232\201\311\250O\0078\252\362rs\212\205\233\003\201U\236\241z\201\333\214Uw\250\032\241j\205\252\321jijaji4\252i\304\322\253T\252\324\355\324n\247\007\251\025\252Uj\231\032\254,\234T\210\334\324\301\360\335jM\340\236*Ej\2206i\301\260jEl\234\232x8\251\221\252ej\235\033\246j\326\365\000c\255;u(l\323\201\251\003S\203\323\203\324\201\351\352\3652\2759\233\"\230\215\207\253Fb0iL\231\031\024\365\223\tM\017\232k\034\323\010\342\243cQ6OJ\214\241=\351\214\204Uy3U\3335\003\325w\250^\240aP=@\340\212\254\365]\352\0075\003\232\201\315@\365\013T\017S\223L-M&\232Z\200\324\355\364\241\271\247\207\247\007\247\206\245\006\244V\305J\255R\243\325\200\371\002\246\215\307B)\340\374\330\0257\335\305J\215\305H\247&\244\347\322\234\032\244\014*U\315H\255\216\265:=L\036\244W\355R\003N\337N\337J\032\236\036\234$\251U\352tz\227vEF\371\007\"\246\211\303\'5*\256\345 \036\224\210\340\214\032yB9SI\311\353Mj\211\215D\336\324\336E1\213\n\205\333\332\253\276*&\013\216z\325f\343<Uw\250[\245B\302\241\224n\252\262%Uu\305WqU\334T/P5B\325\013\323\213S\t\246\223I\2323I\232pj]\364\340\364\360\365\"\265I\232z\265N\246\246\rS\241\356:\324\321\311\216\243\232\220\023\336\244V\251\003T\313\'@zT\203\004R\240\311\353S\253\355\340S\367\344\212\2205J\257\212\2208\251\026J\223viwR\207\247\007\247)\251\225\252Uj\234=;9\246\357h\217\003\"\246I\207U\342\225\324\246\037\261\251\221\362:\322\223\336\242f5\033\034\324g\"\232}\350b\010\252\354*&Z\201\205@\342\253\270\250XT\rP=W\220Ui\005V\220Ug\025\003\212\205\205@\342\241aL&\232M4\232L\321\272\2234\233\250\335NV\251CT\212\325(jz\265N\255S+\342\245V\251\327\234{\324\341\370\332{T\212}jQ\202x8\247\364\034\034\323\324\364\317J\221\037i\310\251\003\356l\232~\374\267\024\360\334\324\201\252da\216M<5<585;u\000\324\212\325*5J\257R\253\324\252\365/\017\326\223\312\301\371N)e\220\354\t\327\232\2263\201R\026\342\230G\031\246\026\035\206h\013\270g\275B\352W\255G\273\025\033\021Q7\025\023T\017U\336\240z\201\352\026\250\034Ui\005VqU\234T\014\265\003\212\205\205B\302\253\023M&\232M4\265&\3523HM\000\323\201\251\024\324\252\325\"\265H\032\244V\305L\255S\243T\350\370\251\223,x\251\225\261\326\244\rR\003\351R\253\345pi\350\t\351O\r\212pjr\265J\032\236\036\244W\251\003S\303S\303Q\272\236\032\236\032\246V\251\025\371\251\203T\361\276\0075(j\205\2372\014\366\251\321\352V8\034SKn\030\246\001\212_\247\024\016xj\2574E\016GJ\256y\250\330\324,j\007j\201\315WcP=D\331\250Z\241u\252\356\265^E\252\356\270\025]\305@\302\240qT\311\246\223L&\233\232J\\\322\023E(4\360\325\"\265J\246\244SR\003R\251\251\321\252tl\324\350\307\265L\030\2202jEj\225H\357Rdg\212\221\033\007\"\244$\261\317Jr\343<\363K\236x\247\003R+T\212\325 j\2205;u.\352pjxj\221Z\245V\251\321\252Uo\230\n\262\274\324\027\n\335S\255@\223\310\207\346SV\222\3600\301\340\324\261\271bI\351R\003\232:Q\327\353J\300H\244\036\265I\320\2515\013\212\201\205W\221j\006Z\205\326\240e5\013q\326\241qQ\267J\256\342\253\275Vz\256\302\241u\252\356+<\232i5\0314\334\321\232Pi\017Zu\024\340i\352jE5 5*\232\225ML\206\247F\305Y\211\271\346\246SOB;\323\267b\254\"\202\231\3174\261\261\006\254;p1K\021\014piH\305\001\251\342\244V\247\206\251CS\203S\303R\203O\006\244\rR+T\310\325:\034\221V\221\252P\271\243\310\004\321\366A\235\330\253\n\243n\010\305A(1\237Z\204\315\203\2021NI\206jRy\014\275)\222\000\334\325WZ\201\220T\016\225\013F{T\016\225\t\030\355P\270\007\255V\221qP0\250\331sP:qU\335*\273\255VqU%\006\262w\323K\323KSwQ\272\234\032\235\232\001\247f\224S\305<\034S\301\251\024\324\312jd5:\032\231Z\247V\251\024\323\301\311\251s\267\200sR!\365\342\237\270\376\025&\010\000\323\321\371\245lg\345\247\002@\346\236\017\24585H\036\236\036\234\032\236\255R\003O\006\244V\251\025\252\3025Z\211\363V\220\324\353R\nx\024\025\r\324T\022[+d\342\263\246\267x\330\225\351K\024\344|\257\305M\272\242j\201\205B\342\242\'\025]\352\273\324\016*\027\\\212\256\302\243\"\242qU\244\025ZN\365Y\305D#\311\311\2546U\r\301\310\246\230\201\357BC\273\275G2\252q\236j\024\345\261R\310\2331\212h4\274\216\264\3455 4\341N\3158\032\221ML\246\246SS\241\251\224\324\313\310\310\251\024\324\200\323\324\324\310A\353S#\214`\324\252\343\'#\212L\363\300\342\227w9\2517\226\306iA\247\251\247S\207\024\345lS\303T\201\352Ezxj\221Z\247\215\352\3227q\326\256D\371\025eMJ\246\244\006\226\203\315D\350\017Z\245s\007\004\257Z\253\034\204pz\212y9\250\332\241qQ0\250\034T\016*\006\034T\r\301\250Xu\250\033\203P\271\252\317U\334f\241)\232F\\q\\\256\r4\222)\003\262\3645\033e\2174\344m\224\346\224\2767S\323n\340j\304\221\243.\340qQ\004\\q\326\215\244S\226\235J\rH\246\246CR\255N\206\246\\\232\263\033\014R\347\232\2205=MJ\246\246SN\r\203R\371\271\024\356\n\217Z\010\333@jxjpz~\372pjx4\340jEj\221Z\244V\251\221\252\324OV\342c\273\025q\rL\246\244\006\236\r\031\246\232\212^\225\224\343\023\034S\373S\032\242j\211\205B\342\241qQ\025\315W\2250j\263\212\256\375*\007\346\240aQ\025\246\021\212\255!\346\271\242)\244S\010\244\333M+I\266\224\np\334x\315=r\rNHd\344\363LZ~(\035i\353R\245L\2650\251TT\311\301\342\245\335\223O\006\236\rJ\206\246SO\006\234\r<7\024\273\210\024\375\300\217zvW\034u\247.\336\364\270\317\335\244\r\203R+\324\200\323\201\251T\324\212je5b6\301\253\221\276q\216\265r6\316*u5\"\232x4\354\322\023PJx5\234>g&\236j3Q5F\302\242lTOP\236\rG\"\344UI\027\232\252\342\253\313\305V2`\323K\203U\335\362MW\222\271\242qHZ\232M&h4\001KN\002\237\212Jr\324\202\223\2758T\253S\251\251\243\035\315H*d4\360zT\200\323\301\251T\324\212\325 4\340\324\355\324\271\247\003N\006\237\237JP\304\016)\353\2029\353I\312\232z\275H\247\232\224\032\231\rL\246\245V\253\220\2660j\344g\025aZ\244V\251\003R\356\244-PN~CU\020b\221\2150\232\215\252&5\023TMQ\232i\344Uy\026\252H\265B\343\200k8\276Z\230\362\021ML\221\232d\225\3160\315DA\024\334\321E:\227\212QO\035)*@0\0058t\245\305-=*d\367\253\010r8\251\000\251\024\323\305J*E\305;#\265=MJ\r85874\270=\216i\300\221\326\234\0334\340y\247n\315(<\324\231\365\353H\016\rJ\017\245J\247\212\221\032\247SS+U\270\030\021W\021\252uj\225Z\236\032\227u\005\252\t\233<T$\342\230MFM0\232\215\252&\250\310\250\310\250\311\305B\346\253Hk>\353\2258\254\365\213\004\223PL2\334S\320aj\031k\234ja\250\310\301\246\232ZQN\353J\005<t\244\247\216i\353N\245\3059je5b/Z\2274\240\324\212jE5 4\340i\340\324\200\323\272\032:\322\362:S\203\032xl\323\201\247\003N\006\235\270\223N\316i\352qR\241\'\245H\207&\247SR\253U\250[\006\257#T\352\325*\265<r(-Lg\307\326\242-\334\323\t\246\023L&\230i\206\243ja\250\333\245@\365VW\305Vy*\214\244\263{T\017\307J\252\303-O\306\005V\224\327:\324\303L4\303IN\006\236\r8S\207JJr\324\203\212x\245\245\025*\325\210\317\002\236)\342\236\016\rH\r85<\032\220\032x8\247f\224\032v\352Pi\340\322\346\236\r8\036)A\247\203O\006\236\255R\247&\247\004v\251\024\325\3301\216z\325\244j\260\255R)\251C`\363Li\007\'\265G\273\'&\232[&\232M4\232a\246\323M0\323\010\250\330UyN\005e\\\312rqU\203\356\024\326\305V~j2\270\244n\225Rc\326\271\363Q\232a\246\232i\245\024\341O\006\237\221\212JU\342\245\316qNZu8\nz\365\251\326\244\006\236)\342\234\r8qRn\315<\032xjp4\354\322\346\234\r<\032\\\323\324\323\263J\r<\032vi\353R\243b\246F\343\025<c$\003V\224c\245N\222c\212\264\215\305J\207\232s?5\031$\375(\315%\024\204SH\246\232n3HV\2435\004\215\212\316\270\230\034\200k6f\004\324C\332\220\212aZ\211\352&\351T\346\357XMQ\032\214\232CGZAN\035i\302\226\224\032vx\251\242\0314\3420i\302\236\0059x5*\324\253O\024\341N\025 \247\np4\340i\340\323\301\245\245\025 \245\247\016)\331\245\31585(jxjz\232\231\032\254\304\325r6\350z\325\205\000\266EXSR+S\272\321\212P)qF)\n\323J\323N\0050\232\215\232\240\222P\240\344\326]\335\340\350\reKpOJ\215r\375jP\270\244j\215\215WsQ=T\232\260\232\243j\210\322Rt4\235)\342\234)M(\247\212\222#\206\253.\001\031\024\300*E\024\270\251\005H\225 \245\035i\342\244\024\354R\323\205<S\326\235@\251\001\247\nPih\245\006\224\032x5\"\232\231MXF\253P\276:\325\224|c5eZ\244\006\244\rN\006\234)\331\244\2445\031\2463\001U\345\235W\275g\334_\355\007\006\262\246\277f\316\rSfy\r\002/Z\225W\024\343\322\242cQ3T\016j&5Z^\225\204\325\033Tt\332i\344QJ*U\240\214\032QO\024\364\034\212\262y\300\245e\332i\313N\002\234\005H\240\212\224\014\323\261\306)G\025 \247\203E(4\340jU4\354\321\212u<\032v)(\024\352\0058\032z\232\235\032\247CS\251\351VCg\034\346\255\306\334\n\2305H\2475*\323\300\247b\230N*&\220Uy.@\3175Rk\265\037\305Y\267\027\231\350k9\313\312}\251V\337\035jA\020\024\245E0\323\030\324\016\325\0135D\306\231\236*\274\335+\021\205D\325\031\246\032m(\245\024\354\372R\203\332\235\214S\305H\275j\310q\263\035\350\352i\340T\202\224\n\221jT\031\2470\244\357O\024\340i\324\231\305(<\324\212\324\360j@iisK\232\013\021N\355\223A\342\200iA\251\024\324\252jt5:\266*\302\2779\035j\304.s\212\266\255R\251\251\224\324\242\231$\252\200\344\342\262\256\265UN\024\325\003~\362\0363P\310\362\037Z\200\243\267SJ \035\352A\030\035\250*\0054\212\215\215@\315\212\211\232\240f\250\031\271\2461\250\313qPL\334\032\310j\205\205Fi\246\232E%\024\242\234\016jE\367\251\027\322\245\013\307Zp\253p\306\031\t\364\244\3075 \031\247\005\247\201S(\305!\240S\251\302\235Hi)\342\236)\352i\340\322\346\224\032\0174\354\2221J)wQ\332\234\2652\032\260\225\"\236jp\340~U,r\200\3035z7\315XV\247\371\252\275N*95\024@y\254{\273\366\231\260\235*\232\304\\\345\3715ec\000T\235\260E!@G\035j\"0i\013b\230Z\242f\250\235\252\007j\201\332\240v\250\213S\031\252&n*\274\255Y\306\243aQ\232\214\322\036\224\332(\035i\342\236\265*\212\231E?mX\200\237\272;\323\366\025<\324\200S\200\251\024S\263IE8S\305-6\227\024\242\236:\323\205<\032\\\322\206\245\006\235\232Z^\264\202\236\t\251\227\246jdj\2234\370\316[\025$\352c\034u\244\217P\300\303pEL50\007\025^[\311%\345sP\215\357\367\215J\251\212\225x\247\206\240\2657v)\215%F\315\273\247Z\205\233\025\031z\215\232\241sP9\250\030\324,\324\315\325\023\265W\220\3253L5\023S\r!\246\232J)\342\236\2652\324\350*]\265f\331rH\003\232V\316\356i\352)\370\247\201\232\\`\321@\024\016)\353K\364\245\307\024\224\242\234)\302\226\224\032Zu.i\374\020(\317\006\200i\342\236\246\245\rR\006\247+\340\361VZA*\017QU\236\020\306\225b\003\265I\214\014P8\245\335F\372<\312<\312B\324\306j\210\276*3\'\2551\210\307\025\0215\023\034\324Lj\0075\003\032a5\023\232\255#T\rQ\032\214\323M4\323M7\275-<S\326\247J\260\202\245\002\254\333\022\255\305:C\227\310\247\250\247\342\227\265\004~\224\240\0222(#\201K\214\nQNQ\326\227\024\230\243\024\264\340i\324S\251A\245\245\351K\316y\242\234\032\236\r<\032xjxj\261\033|\206\231\346sN\337\232pl\365\246\027\246\357\244/I\276\215\364o\246\027\246\026\246\023Q\261\246\357\3051\271\372T.\010\252\356j\0265\0315\013\032\256\346\243j\211\2523M4\323M4\332Zx\251\026\246J\263\035N\242\254\333\256\343\216\346\245\222\023\030\311\246-IJ(\31794\240q\301\243\2674\270\247\001J(\245\024\237J\\R\201\232u(\240zR\347\024\341\322\227\255-\024S\201\247\006\247n\245\rOY\010\342\215\374\322\211)\376g\035i\205\350\337H^\223}&\3727\322n\246\226\246\026\246\226\246\023LcL-\236*\031\024\212\200\367\250\\\324,j\007\250\330\324f\2434\303\326\232M#SE8S\205H\265:\n\263\035N\2654Rl>\225l\376\372\"T\222E@\243\006\245\024\224R\212p\346\224S\251\324b\2121E(\245\242\234(\306iz\032ZQK\212(\245\006\215\324n\243u.\357J7R\357\240\271\305\001\350/M\335F\372]\324\233\251\013S\013SKSKSKTmM2z\363PI\355P1\250^\240z\215\215FM0\323\r4\323Z\212QO\025\"\324\311V#\253)O#5j\331\202\251\365\246\377\000\025?<SM&i\300\323\251\300\376\024\341O\300\365\245*G=i:\322\322\342\223\030\245\0034\001\353KI\322\226\234\264\352BqJ\r!\244\315\031\030\246\226\"\215\324n\243u.\3527Rn\243u\033\250\335I\272\227u4\2654\2657u\031\246\223LcQ1\246\026\343\006\241\221p2:T\rP\275D\306\243&\243&\232i)\244\346\212QR-H\005J\265a*\302\032\222\254[\270V\344f\234\347,H\242\220\322QN\006\234\r8\034T\213\3158t\243\003\034R\212\\Q\266\212)GJCE(4\374\322\023M\315\004\346\214R\036)\244\322d\322f\223u;<P\r\004\322n\243u&\3527Q\272\215\324\322i\244\323sHM4\232a5\023S7v=*\031\007\247J\201\252\273\032\215\2150\323I\244&\233\232\005<T\212jAR\255N\225a*A\322\246\204e\205J\343\r\315&i\t\246\320\r;4\340i\300\324\200\342\237\232\\\343\2458\014\232p\030\245\002\220\212L\322\322\036\264\224R\346\202sM&\214\322\203E!\024\302i\204\322\203\232\\\361\212L\321\2323M4\322h\335F\3527Rn\244&\232M!4\322i\204\323\t\250\332\243\'\326\242qU\030\324f\230M4\232i4\231\245\245\025*\324\200\324\250j\302\032\235jQRF\373H\305Lr\347\216i;\322\036\224\323I\232\\\323\201\247\251\365\247\203R\016\224\361\357R\014R\321\320\321\326\223\034\321Hi\010\315\035)3Fi\244\321\273\336\233\236iwzP^\230\315L\245\315\031\2434\240\320M74\322i\244\321\272\2234n\243u4\232nh\3154\323\032\243j\215\2522}j\2214\302j3M4\322h\2434\361OSR\003R\251\253\010j\302\032\224S\207Z\274\233V\023\317&\253\347\232Ri\246\231K\232p4\360i\352x\367\251\007J\220\032\220S\261K\214\321\212)1Hi;SM%!4\322i\245\2517Rf\2234\322h\315\031\367\244\335N\316G\024\252q\326\220\232ajBi\244\322\026\244\315\033\250\315\004\323I\244\315\031\246\236j6\250\215F\325@\232i4\302i\204\322f\214\322\323\201\247\003R)\251T\324\350j\312\032\235iI\305[\265\304\200\2065\034\213\261\310\240\021McL4\200\323\201\251\024\323\324\324\240\361O\006\244SO\006\235J\r.8\246\237jCHi\264\323L=)\244\323OZ\030\00085\036h\315!4\231\2434f\2246)X\372SwqL&\2234\322\324\233\2513Fh\315\031\244&\233\2327PM0\324mQ\232\316&\230M4\232a4\231\245\240S\307Zp\251\005H\246\247CV\020\325\2054\254x\251l\345\304\240\032\275=\271\220\3461\237\245Tu1\360\334T{\263M&\233\232p5 5\"\232\221MH\246\244\006\236\r8\032\\\323\267qIHi\246\222\232i\206\232i\215L&\2234\231\244\2434Rf\220\2327SI\244&\220\232i4\334\321\272\214\322f\2274\231\244&\2234f\220\323\rF\325\226M0\232i4\322i3KN\024\361N\024\361R-J\246\247F\253(\324\342j5b\216\010\255\233=McC\270U\033\253\257>B\325\016\352B\324\231\247\203R\003O\006\244SR\003R\003R\003K\232\\\322\346\2274f\220\323M4\323I\346\230M0\323\r4\323sFh\242\220\360i\271\2434\231\246\223I\232i4\334\321\2323KFi3M&\2234f\214\346\232MF\325\222Z\230Z\230Z\223u\031\245\006\236\rH)\300\323\205H*E5*\265L\257O\337HM cN\006\235\272\220\265\001\252E5 5\"\232\220\032\221MH\r<\032p4\264\240\322\321Fi\246\233Mja\246\232a\246\023L&\2234\240\322\346\220\236)\204\323sM-Fi\t\244\315!8\244\315\031\245\315\024\204\323I\244&\2234f\202i\215X\244\323\013SsIFi\300\323\324\324\200\323\301\247)\247\203O\006\244\rO\rO\017N\rK\232]\324n\2434\240\324\212jE5*\232\220\032x5\"\232x4\340i\300\323\250\315\024f\214\323MFi\244\323s\351L&\230\306\230i\271\2434\240\323\331~\\\212\210\234S\017\037Ji4\231=\251\271\2434QE&h\315\004\323I\2444\231\244\315\033\275\351\t\254&ja4\334\321\232\\\322\203R)\251\001\247\203N\006\236\r8\032x4\340i\301\251\341\251wS\203R\346\226\234\r<S\301\251T\324\212j@i\300\323\301\247\203N\006\236\r.i\t\244\315\031\244&\230M0\232a84\322\336\264\322i\215Q\232\006M(4\340\347\030\244q\305DM0\232M\330\246\226\240\032v}(\315\035h\351Hi3IHM0\322f\2234\026\254\026ni\244\322f\214\321\232p4\360jE4\361\322\236\r8\032p4\360i\300\323\201\247\003N\006\224\032p4\340i\340\323\201\247\203R)\251\024\324\200\323\301\247\203N\3158\032p4\354\321I\2323HM0\232\214\232a4\322i\244\323I\250\311\244\007\035i\336\342\223uI\346\002\270\"\241c\315DM4\232@\330\315(\351\305(4f\2274\271\244&\222\220\323I\246\223M4\334\322f\260Kd\322\023I\272\214\322\346\224\032z\232\221O5(4\340i\300\322\203N\006\234\032\234\032\234\r8\032vi\300\323\201\251\001\247\003O\006\244\006\236\r<\032\22058\032x4\360i\300\323\263Fi\t\244&\214\323\030\324li\204\323\t\246\223M&\230M4\236jEn9\2466G=\251\003qL-M&\230M&i\331\030\343\255.sFOz(\315\033\250\316h\246\032i8\244\246\236\264\206\271\342\334SKR\203FiA\245\006\236\rJ\206\244\006\234\r;4\340iwR\206\247\006\247\006\247\206\247\003N\006\234\247\232\220\032p5 4\360i\340\323\301\247\203O\006\236\246\234\r8\032v\352\\\321\232ni3HNj6\246\023L&\230M4\232i4\302iA\342\244\335\270TG\214\342\230M4\232ni\t\240\032x<\361O\335\232C\3056\2234f\214\322\023M\'4\204\322f\232k\233-I\272\215\324\240\322\346\234\r9NML\r<\032x4\240\322\346\215\324\240\323\203S\303S\203S\203S\301\251\024\323\301\247\203O\006\236\r<\032x4\360\324\360i\340\323\201\247f\234\r\031\2434\204\322f\220\232i4\303Q\232a4\322i\204\322f\214\322\206\301\346\2069\025\0214\334\342\233HzQ\236)CS\325\251\375E34\323II\2323Fi)\264\231\256_4f\224\032\\\323\201\247f\236\265 4\360i\300\323\201\245\315&i\300\322\203O\006\234\032\234\032\236\016jPi\340\323\301\247\203N\006\236\032\236\r<5=MH\032\22458\032vh\315.\352Bi3\357I\232L\323I\246\032\214\323\r4\323s\326\2234\023K\234\214S\032\230M%%&h\3158\032z\2659\271\346\230i\206\220\2323Fi\r!4\225\312f\2274\240\323\201\247\003N\006\244\006\234\r8\032x4\240\322\346\214\373\322\203N\rJ\032\234\032\244SR!\251\001\247\206\247\206\247\003N\rO\rOV\247\206\251\001\247\203J\032\234\032\235\2323J\032\202i3Fi3HM0\363L&\230i\206\233\330\323s\315-\004\323I\374i\207\332\232M&h\2434\003N\rO\rIM4\323\326\212(\244=i\246\271,\322\203N\006\234\r8\034\323\301\247\003N\006\234\r<\032]\324n\2434\271\245\006\234\032\234\rH\rL\r<\032p4\360\324\340\324\340\324\360\324\360\325 4\360i\341\251\331\245\rN\rK\232\001\245\315&h\315&i\t\247.0sM+\270dT\0140y\246\023M\355M\'\2323A4\323M&\233I\301\240\373RQ@4\340\324\340h\246\236i\017ZJPh4\206\270\372p\247\003N\006\234\r8\032Pi\331\245\006\235\232\\\322\356\240585.iA\247\203R\241\251\001\247\006\247\206\247\006\247\206\247\003O\rO\rR+S\301\247\206\247\006\245\31585.\352\\\321\2323Fh\315\006\223v(\017\214\212B\233\206sP\260\3052\230zSM\031\240\234\323M74\032LdqI\232J)sN\rK\234\321M4QE\025\306\212p4\340i\300\323\201\245\315(4\271\247\003K\272\227u.\352\003S\267R\203N\006\236\rJ\247\024\360i\301\251\301\251\340\323\203S\301\247\206\247\203O\006\244\rO\rN\rK\232pjPisF\352\\\373\322f\227>\364f\212m\001\261Ms\232\214\323\r0\232nisHM6\212L\322Q\232L\321\232\\\322\203KE\024\235\351h\2560\032\\\323\201\247f\224\032viA\245\315(4\003K\232]\324\003N\rN\rN\006\234\246\245\rO\rN\335N\rN\rR\003O\006\234\r<\032xj\220585<5(4\340iCR\356\245\315.i3K\2323Fi\t\244&\232M4\232\214\323M4\232L\321\232)\t\240s\305%4\234\321E\031\2434\340h\315.i(\242\270\300iA\247\003N\006\2274\240\322\346\214\322\356\245\315\033\250\315(jv\352Pi\341\251\340\324\201\251\301\251\300\323\303S\203S\303T\201\251\301\251\340\323\301\251\003S\201\247\003N\006\235\232\\\323\201\245\315\031\2434f\227>\364g\336\2234\231\244\3154\232a\246\323M!\244\315.i\r\024\037j@3IHi)s@4\354\322\321E\025\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\010\244IDATx^\355\335\211r\343\250\026\000\320\224\373\377?9\256\031\333\261\343\310Z@\002\304rN\275z5-K\010.\227EJ\267\363\365\325\266\353\364@\264\313\364\000\000\334\214\263@\034_L\001\240I{\227\300q6\tc\333\233\037\000\000\264\304\356\276\021:\352\240\016\236o.\222\000\000\000\006\322\301C\014\201<\354\301AM\017\242\313y\023\376Y\367\345\227>\030\234\004\340N\"\000E5\275u\206\303\256\327\nV\336\3470<\241&\'\334\362t\225\266\271T\265\336\356S\352\226T\306\302\017\000\3000\376<\365D>\377\337O\216\272\002\242\310\256l\016=\366\036\272\230:\351Tj\362\234\374\345\345\240\254\376\000\371\305\316\265?\347/^\266\370\301\016\347n\000R\266\244s\347v\024\345\\7G\205\\\200\256\254\217\371\365O\2419\326\260\235z\t\\/\355\330i\360\346\177\215\033\201\200w\357K\217\000+\227\354\267p\257_\233\'\354\223\251\330.=bu\375\372\367\236\001\007\003\370H\262\333\377gI\2537\007+\372!w}a0\251\207\350_\006lv\331B\23473\340E\252\001,Z\233\"#\267\0001\247Oo\033s-\211=\202\177\371\372\236\034\237\232v\322\264\023\243\035. \334\322\033\260\321M\373t\206\310\301\235\241\000\260\311TIE\244\343\246\303!\nx\226\370\343\360\r#\224\274W\007\256\361\275\031 C\221\201.2 Z\300\357\364l%\246\263-\271\037\014h$\351\265\2228\225\0216\240\214\214Kc\306\242\367\252\260J\220G\360Fbrb\372A\022\\\223dnw\374mG\371\3737\345\021\250G\220\362\206\352\377\322\237?\244\377\351\235\340d\313[\261:\004\007\2431s}7w\214\332\365\232\241\0041h\233\224\270\333\022\027G\313\316X\021\316\270\'U\221\002\305]oQ\177\306\375\300\032p+\342~\371L\031\227\231c\207L\0373S\227\237Yc\325\335V\313\317b#\177\267\306I*\t\026t\302\210:*\305\274\231\242\014\330 \3152\022\\Ng9/\357}\340\357\216\177\231\331c\366m\305\373\037\312T\243c\367\010\177<\324O\377\274\350-\376i\272\"M)l\272D\364\362\231jI\210&\202u\323LE\003\325\222\000\024\360\231\274\237G\252\325PU[!\244e\375|?\364\364\360\026\335D\016\323o\004\223g\235\233}\340g\034\021#<\342\324t\256\263wm\345a\272\025^\361\035\324S6N\022\240\227|\310\334E\231\213\317\245\321j\327b\272a>\333\376\356\334\177%pH\225\203\257\262\251\255\177\227\023b\036z\303\327y\211S5\364\376\275I\034\306Wy\251\313}\312U\356K\366\033\264`\324\321\000\037\014\006\306r0\343\017^\276\310\322\014\220T\360\264\032|bn\327\327\032SM\225\032\262\024\263\245\343i|\374[\2034\362\224\032\346v\357{\036\236Y\211\302\006j\352\036\231\262\234\212\215\326\345\271\036\3578\323%\274cC\317\243k\227\361\246>\350\313\357\243\364\333\221m5\014\374\245:\334\366\340A\215\240\037:\374\220\370\360-\215\276\342\342\253\016\300\274\364S{\372\022\311\350\325]\326V\302\370\301G_\214\374\341\214\333\345\346.\346\311\214<\262\3155:\214td\023c\371;1\313\377%\003E\246\305\246^\037\265\316\266\315\030\312F\002\314~|\213\374\354\007\231=\357\371\375\347\350\303\341l8\243Ag\t\014VHH\002\213\312b.\0178EH\252P\205T]u\346\310\'\241\310\216L\225?T\241\325\356\214\314Z\226\034H\200\003\227&TG-h\200Ic\215\350\214mi\"\275\347\305\334\267\020\317\035\203\362n\251\273\224\276\344UM\3347+b\276\n\323o\230\372m\031\214\252\245Q]w]\227\227\320\345O\316WwL\233Rs7/\370\255\262\177\250RLOC\256\247\266\000\000df\303\315\017\273h\316\"\367r\272O\362!!\356\352\347k=\265\005\010`\320\307\020\255A,\375`E\002\014h)\031*\325Vm\213\022\0326t\230\"\317&u\330\264\014\032\233\355C\255\356\\\356M\356\263\335\303\3534\237\353\266:\332h\307\336\216\334{\035\365\212\232H%\000\014-j\276(\305\274\224\334RH\257\313\0371\222*\277\267\257\253\277\273\000\000\241r\274\030]+\362\343\263\217\003\347\261\027H$GR\001\355iy*\270\327\335\252\020\353\362w\001h5\200\337\267\212\377\274 \270\264\333\214\363\265\033\271[\036\267[\373\032\264<\375\1775_}\356\222\366b\322\302\022\253\264n}M\241\247\006y3\224\247\326\256{uDw3\t\250\333\301\016\\\272|.;\347\216\245\260T\207x\351JjG\3126\317<k/\024\237+\027\206\260\020\323\363}V\354\363H]\374| \277\231\020\317\034\242\177\277\263A\235\tP\373l\005\344V\347\334D1\022\240\2544\361n\357/P?*\334h\265\017\230\351\357\303e>\335\313NV\0327\257p\212+\000\220X\242\355\305\314\366\222`\211:\201rtYaf\230\206u\327y\377\372k\0221.\022\340T\242\017\230\010`xf\001\350\310\216\367k\346\200\236D\'@\364\005\324\315x\236\032,\"\2035\027\210`~\200\\\306\332O\233K\022\020\304\303\204\020R0\222\000\000\310\256\350\246\263\350\315\200\342\306z\013\013\345\031c\000D\032\355\021l\264\366\206\312\025\227\327\326$\327\r\240\022R\034>\030\026\000\000\205\005\377x \370\304E\037\277O\341{\362g\212y\337w?\376\273\334N|\232\006P\251r\203\002\000\200\363\331\375u\306\223\347\311&\035\240?\006\327P\002X\014``\r\315U\000\225\351b\013\265s\031\350\242\355\275\320\031\033v&\371\207{93\205=;@G0\034IO;d+\274\271\030\021\207<\267\2033\333B\250A@j\0161\t<\246\272\350\246\006\204\217&\374\364d\262\016\215N%\316\225\252\347u|\363R\245B_\306J\354\261Z\233\226\3301\022\353EeL@{\255\246\362xa]\r\307\233\311y\327\351\001\232\264\222\360\037_\361\360i\373\014*\267\222\000\237\364\367P\002\272;\340\024\240I\177Fw\324JA\027\226\246w\271\360\264\024\241\023\235\3319\025\206\343\200\276Z3k%Yv\265~\327E\355\352\247\271+\211\020\255\237\250\214c\265\317V?\234J\231I-k>\016\3157\340d\342\007\264+j\335\357\226y\234\036\254\216\346\225\017\223\344\377\277\351\201v$i\177Y\025V\371Y\245\n\253\306\'\3354\234\253N\007\312ha\262Y\331\025\267\255\205\340\367C\264\233\367\234\t\032~\216\335\257\333i0?\241\013\362\372\327\006\325O\225\325W\020\000\000v\261\323]t\211y\254\21397\271\2147\317V\364\275\340l\245\0374\251\327V5\347>7\260\2327\327\255S!\347\320\255\327(7\334G\'\003\000\200B<\200\320\213\2507nP\234\207\274A\351x\300L02}?\274\200\257L\337\"\213\032\027\233\002:\034:b@\303\270b7\000\r1\265\225\2623\211v^\006\320\201z\226\250\357\235\223\361l\013f\017R\267\235\t\300nUE\274\252\3120(+\307\340$\000@\303\354%\001\000\000F\343u\036\264\307\270\005\362\363\256x0\226\226f\350*r(0\347\027\270\005\014\342{z\240vV\256\204\022|Q\004\0041p\273w\321\3074A\242\002\0000\252\337w@v\3050,/\203i\225\245+\0053\300\227T\202\202\016\016\267\304SV\342\342\032v\260_Z4`\223a\312\034\0100\260?\213\200\025a<\372|`\257\316\257<\013<\261\345u\275\377O\224a8\225\317\375\024\"\017\250\217MI1u\206\272\316ZAG\254\375\300@.\266\026\000\000\000\003\272.<\013z1\006\000\320\232\205\215\035\314\221.0,\303\037I\000\000\000}\332\263\325\337s\r\320\000\203\033\000\000\330\340wb\017L\337\003\000\000@\223<\322\017N\002\000\000\000\000\000\020\311w\n\003\214\3516\377\373\361b\333\364\037\000\000\300P\274\312\005\000\000\000\000\000\200\221\370\213\242\000\000\000\000\000\000\000\000\000\000\000\000\320\013\337\'\010\000\000\243\363T\000$\364\037\213Ab\036\317\"\"\006\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-9.textpb b/core/res/geoid_height_map_assets/tile-9.textpb
new file mode 100644
index 0000000..5397cb37
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-9.textpb
@@ -0,0 +1,3 @@
+tile_key: "9"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\363<\322\206\247f\234\r(4\271\2434\271\367\243u.h\rK\272\214\322\356\247\006\247\006\346\244\rO\rN\rO\rN\rO\rO\rO\rO\rO\rR\006\247\206\247\203N\rN\006\234\r8\032\\\321\232]\324\271\2434f\214\322f\214\322f\232M4\323I\244\2444\224QE\024\200\3434\235i\r%\024S\250\242\212up\364f\234\r(4\354\320\r.\3527Q\272\2274\003K\2327R\346\224585<5<5<5<585<5<5<5H\032\236\246\244\006\236\r<\032x4\340i\300\323\201\245\006\2274f\2274f\214\342\214\373\322f\214\320M%6\220\365\244\244=i(\244&\200sKM\246\232(\242\212QKE(\024\265\303\323sJ\r;4f\2274n\245\315\033\250\315(j\\\321\2323F\352pjxn)\301\251\301\252@\324\360\324\360\324\360\324\360\325\"\265J\255O\006\236\r<\032x4\360i\300\323\201\245\315;4f\214\322\347\336\212L\321\232\\\321HM%!\024\224\206\222\212i\245\024\264\332i\242\212(\245\024\264\270\245\351Hk\2074\224\231\346\234\r\031\2434f\214\321\2327R\356\243u.\3523F\352]\324\340\324\360\324\340\324\360\325\"\265<5<5H\rH\246\245V\251\003S\201\247\203R\003N\006\236\r8\032]\324\354\322\346\214\322\321I\232\\\321Fh\242\214\323M!\244\242\223\275-\024\230\346\222\233E\024S\251@\245\242\232k\2104\224\206\214\322\203HM&i3Fh\315(4\271\2434f\215\324\340iA\247\006\247\006\247\206\251\003S\325\252@\325 j\221Z\245SR\003O\006\236\r<\032x4\340i\300\323\263N\006\2274f\235E\031\242\224\032Z(\242\220\322\032LqII\336\226\212)\264\204RQJ\005-:\212CI\\I\024\332CIE\024\206\220\322f\223>\364\271\2434\271\2434n\245\006\224\032p4\340i\341\251\341\251\341\252@\325\"\265J\246\245V\251\024\323\301\247\203O\006\236\r<\032p4\340\324\340i\300\320\016)\300\321E(9\245\247QE\024Sh\2444\224b\227\336\222\220\322R\001\212Z)@\245\242\232h\256,\212a\024\323IE\024\332i\246\346\212L\322\346\214\322\346\214\322\203J\r(4\340i\341\251\301\252Ej\221Z\244\rR\251\251T\324\200\324\201\251\340\323\303S\301\247\003N\006\234\r<\032p4\354\320\r.h\3158S\251\324QE\024\204Rb\212LR\320zSh\246\236(\245\305.(\242\212CI\\{\n\214\212a\024\322))0i)\246\233Hz\322Q\2323Fi3N\006\2274\240\323\201\247\006\247\203OSR\003R+T\252\325*\232\221MH\r<\032x4\360i\340\323\201\247\203N\006\234\r<\032\\\322\346\226\234;S\251\302\212)qF)(\"\223\024b\223\024R\032J)\000\305-\024QHM%\025\313\2749\252\355\031\025\031\030\353M\"\230E%4\323H\246\323i\244\346\214\322f\214\321\232\\\322\346\234\r(4\240\323\301\247\203R)\251T\324\212jU5(5 4\360i\340\323\301\247\203N\006\236\r<\032x4\340ii\302\224S\351\324\3521N\242\227\024\224Rb\223\024PFi\264\204RQE\024\202\226\232z\n(\256}\201\035EB\342\241x\3628\250\031\010\250\310\246\221M\"\232i\246\230i\264\207\255%\024QJ\r(4\352Pi\340\323\301\251\024\324\212jE52\232\220\032\220\032x4\360i\340\323\301\247\203O\006\236\r<S\305.iE<S\305<R\323\251qKE\024b\223\024\224SH\2444\224\206\222\212CHh\315\024\231\2435\222\312\r@\360\372UwB\265\031\301\250Y*\026LS\010\3054\212a\024\322)\206\232Fi(\2444\264R\212\\\323\251\342\234*AR)\251V\245SR\n\220\032x4\360i\340\323\301\247\203O\006\244Zx4\360ii\324\341\326\244Zx\247\np\024\264\354QF(\244\"\222\220\212B)\246\233Hi(\2444\224R\032J+0\212a4\326P\303\232\255$\004Ur\204\036j9W\035*\"3Q\025\246\021M\"\230E4\212i\244\242\212)E(\024\352p\247\212x\355R-J\265*\324\202\236)\342\244\006\236)\340\323\305<S\305<S\305>\234;S\207Z\221i\342\236)\330\247\001K\212Z(\244\305%!\024\323M=i\207\255\024\332)\r%\024\332(\252\014\265\021Zk\014Te\210\250\\\346\240e\3109\250\031\010\250\310\250\312\324dSH\246\021M\"\223\024\230\243\024\264\240R\201K\212p\247\nx\025\"\324\253R-H)\342\244\024\361O\035\251\342\236\264\361R\nx\355O\024\361N\024\361OZ\220\nx\024\360)\300R\342\226\2121M#\024\204R\032a\246\236\264\323IHi)\017ZJ):\320\005-Se\250\312\324N8\250XT,\246\242<S\030\217Ja@\325\023\305\351P2z\324M\221\332\223\031\244\3054\212LQ\266\227\024b\235\212P)@\247\001O\002\236\005J\242\244Zx\251\005<S\305<S\305<v\247\212\220S\326\236)\342\236)\353R(\251\000\247\201O\002\235K\212Z)\010\244\246\322\032a\246\032CM\2444\224\206\222\212(\243\025]\205D\302\243e\250Yj\026\250XTL*\"1I\274\212C\206\250\312\343\202*\027\217o#\2450\212M\264\233h\333K\266\215\264\273i\301iB\322\201O\002\236\005H\005<\nx\024\360*AR\001N\002\236\005<S\305<T\202\236\005<S\300\251\024T\200T\200S\305<R\201K\212\\RQM\244=i\246\230i\246\233M4Sh\240\212LP\005\030\245\252\3548\250XS\010\250\331sQ\262rsQ2T\016\230\250J\324l\264\322(\353\301\246\225\364\346\2431\203\315FT\2126\321\266\227m\033iv\323\202\322\205\245\333N\013O\002\234\005H\0058\n\220\nx\024\360)\340S\300\247\201O\002\236)\340S\305<T\212*E\025 \024\361O\024\361J)\324R\021IHi\247\2554\323M0\323i\r%!\024\224QE\024\240T.*\"\274\323\nSJ\212\201\316\r0\340\365\250\23528\252\345i\245i\205j2\264\336E\033A\351HW\324S\nc\245&\332]\264\273iv\322\355\245\333J\026\234\026\234\026\236\026\234\026\234\005<\nx\024\360)\340S\300\247\201N\002\236\005<S\300\251\000\251\000\251\000\247\212x\247\212p\351N\024\264PzSi\246\220\322\032a\351M4\323IM=h\242\212LQ\212\\QQ\311\307QQ\2023\315D\347\346\"\241c\316\r0\25794\205A\250\210\347\035\252\'J\214\2554\255FV\230R\230W\024\017zw\226\010\342\231\266\227m.\332]\264m\245\333K\266\234\026\236\026\224-8-<-8\np\024\360)\300S\300\247\001O\002\236\005<S\300\247\212\220T\213O\035\251\342\234:S\307JQKE!\244\246\322\032Jm0\364\244\246\322b\222\212(\242\212*2\373\206\rDN)\215\202sLe\3151\224\324y\307\024\322\271\2462\344TEqM+\232C\0354\305Lh\361L)M\332iv\322\355\243m.\332P\264\241iv\323\202\323\202\340\323\266\322\205\247\001J\0058\np\024\360)\300S\200\247\001O\002\236\264\361O\025 \247\212x\355O\024\341N\035)GZu\031\246\223\232)\264\206\222\232i\246\233M\242\212m\024QE\030\250\260\001\246\355\004\320c\301\246\225\246\221\305Wd\301\246\036)\206\243jn)@\247b\220\307\232\215\242\305Fc\246\354\366\243\241\301\245\0034\340\224\241)BS\266R\354\245\333N\333F\332\\S\261J\0058\np\024\340)\300S\200\247\nx\247\212x\247\212x\353N\024\360i\342\234)h\242\212)\264R\021M4\303\326\220\322R\021IHE\030\244\305.(\305-T\rRdu\240\270\357L\334\017CLn{\323\0175\023\000)\207\024\306Q\330\3231F(\247\250\247c=E#F\010\250\214x\355Lhw\366\346\232ad\367\251\025r\005<%.\312]\224\273)v\320\026\227m\033iv\322\201K\212v)qJ\0058\np\247\n}<S\205<S\201\247\203N\024\340i\331\245\242\212)1A\024\206\230i\246\220\323h\244\305%\024QE\025L\214\032c=0\266i\003\342\232_\232\003f\230\355Qg4\034\201MV\311\346\236V\224\n\\S\305<-)L\320\023\006\234\321\002:Ub\2066\351\305<.E8-.\3126R\354\243m\033h\333F\3326\322\342\227\024\270\245\002\226\235N\024\352p\247\003N\006\234\r<\032x4\340i\300\361K\232Z(\242\212CL4\323M\242\223\024\230\244#4b\214PE\000R\342\263\013\346\232M0\234S\013S\013Q\277\024\302\364\201\251\373\251\274f\234\r<\nx\024\340\265*\212xZpJv\323\216)\222\306\032>\225]\007j\227m.\332]\264m\244\333F\3326\322m\243\024b\214Q\212ZP)i\324\240\322\203N\006\234\r8S\305<\032x\247\nZPih\243\024\270\246\232i\246\232i\024\224Q\212n1E\024QF+\017\1774\360\334SI\250\330\323sL&\231\234\232\t\305;w\024\3459\247\014\323\303S\325\215L\207&\246\002\244\002\236\005</\024\320\241\216\017z\202X\274\267\247\005\315;m\033h\333I\266\215\264\233h\333I\212M\264b\214QE(\024\264S\251E8S\205H:\323\205<S\251A\245\035i\324S\250\246\221L4\323\322\222\212)\010\346\222\212)1@\353K\\\343\032r\266E)5\0314\332k\032fy\245oZi5\"\032\234t\246\223J\257\212\235d\350EYI\001\353R\214v\251S\223S\252\323Ja\270\244\236=\313\357P \342\244\333F\332B)1F(\305&)\010\246\342\214Rb\212(\242\212u\024\341O\024\361\332\236)\342\224S\207ZQ\326\235J\0058RR\032a\246\232CIE\030\244\"\223\024Q\336\200=)Es&\2054\342i\204\323sMcH)\343\221\212\215\226\225\016*p\374S\013P\032\244Rj\312?\0305:\260\307Z\2327\301\253\321\020\302\234\313\363S\2312\275*\243.\327\305<\n1I\212LQ\212\010\246\342\220\212LSH\244\305\024\230\244\242\212u\002\234)\342\236;S\305<t\240u\247S\307ZQN\247R\021M\"\232i\246\232i1K\212\\sI\212LQ\214\322\025\346\224\016\324\240W.i\005\004\323O\025\0337\245&iA\247\251\024\244dR\005\243\245!\247-N\270\247n\002\234\262U\210\336\257[\311\203VD\300\232\262\010e\025Z\3456\2604\320(\"\223\024b\214R\021M\"\223\024\334R\021HE%6\220\322QJ:R\212x\353N\024\361O\024\361@\353O\035i\302\234)\302\224QM4\323M\"\233E\024\242\214Q\2121HG4Q\326\271L\323Kb\233\273&\232\317\315FM\000\323\363H_\025*6Fi\331\246\223@\245c\201M\363qHe\315H\222U\210\234\325\270\345\"\255\246H\334*\314R\236\206\211\244/\317\245*\034\212~(\305\030\244\"\223\024\322)\010\244\3054\212f)\010\244\246\322\021IJ\0058S\205<S\307Zp\247\322\212p\247\201N\024\340)\330\244#\232CL4\322)\244Q\212Z1K\2121F)\244P\005-q\346\233\324\320\307h\367\250I\246\323\226\245\003\212c\016jU\030\024\356\324\334\323\226\221\307\025\003)\246\200sS\306*\324b\255\306\271\305[\214\225\\T\360\214\346\247\n\010 \365\250\323\344}\265`\014\322\342\214R\021M\"\232E4\2121M\"\230E4\212i\024\322)(\242\224S\3058\nx\247\np\247\nx\247\212p\247\nZ)\010\246\221M\"\233\212JP)h\242\227\024\322)1K\212\343\013f\232[oJc1=i\271\244\247%L(\306M<\nZi\247-\0148\250\217\"\220/52\361V#Ry\253\221\n\266\243\002\254\300\234dT\354\234dT2/\000\216\325,|\201Rm\243\036\324\230\246\221M\"\233\2121M\"\230E0\212i\024\322)\244Rb\224\nZp\247\212p\351O\245\024\360)\342\236)\302\235N\244\305%!\024\322)\244RQE/jZ)\010\244\242\270|\342\202A\2461\246\347\232Zz\361R\nx\247QM<\323\322\207\351Q\nv)\311\311\2558\020\005\025aTT\3123Va;j\354 8\'\265C,xb;S\"\033X\255O\2121HE4\212i\024\334PE4\212aZi\214\216j2)\244SH\244\242\224\np\247\n\220R\342\235O\024\341O\024\361N\024\242\212)1M\"\232i\270\244\242\235J\005\006\222\212+\200-\315\001\251i;\323\205H\007\024\341O\035){P)\017Z\221(z\217\024\247\245I\002\026`kN1\200*\302\324\312*e\025j\014\203O\230g\004Uc\303\203V\227\221F)1M\"\232E7\024\021M\"\232T\0321\306*\006\\\032i\024\302)1E.)@\247\212p\247\322\201O\024\360)\302\236)\324\270\353F3@\024\224\323M4\332\r \024\264\341E\024\230\243\025\347D\363NZx\351KJ*U\247R\203JM\000\322\023\315J\235(\316M&)\247\322\257Z\307\205\031\253\212*d\025aEL\203&\254\'\007\212\224\214\212\254\313\301\366\251\3429QO\305!\024\322)\244Rb\233\212i\024\322)*6\03750\212a\024\230\243\024b\234\0058\np\024\341J\005<\nx\247\212p\247\nQE\024\206\232i\204R\021IE(\024\264\270\342\200(\305\030\2577\034\323\201\247\203N\245\035i\340\323\301\247\nF\342\2054w\247\226\302\323Q\262jZD\\\2775\243\021\342\254\245N\202\245\310\002\244G\253P\363\315X\306EVu\301>\364\260\235\247\006\254\342\202\264\322\264\322\264\334SH\246\021M\"\223\025\031\034\323H\246\021I\266\223\024b\235\212p\024\340)i\300S\200\247\212x\024\340)\324\275\250\305\030\246\232i\024\206\233I\2121KN\002\212P(\305\030\2575\002\236\0058Q\232\\\323\225\252@i\371\305!9\245QK\212\033\221L\037+U\205\344S\200\301\253q\036\225q\010\035iL\276\224\345rjt\255\033q\2003V\2052H\362*\020\207>\365:\036\306\244\3054\255&\332i\\Tl)\204SH\246\221M+M+M+M\333I\2121J\005.)\300P\0058\nx\024\340)\342\234)\300R\322\342\222\232z\322\032i\024\204Rb\214P\005-(\034S\200\245\244\305y\270Z\\b\212B9\2434\341\326\245Zu\024\364\024\204\363N\034\212FL\362)\312\330\353Rn\315O\023\0003\336\245\016O\322\246\217\236\265aMX\213\226\025\245\017J\262\265&2)\0259\351Oh\3062)\000\310\240\2554\214Tl)\204SJ\323\010\244\333M\333HV\230V\220\2554\255&\332\\R\342\214R\342\234\0058\nx\024\340)\340S\200\245\305\024\021M4\322)\270\244\305\030\240\n1K@\024\360)h\305y\2304\354\346\232N)A\315\004S\220z\324\271\300\245\003\024\243\232\223\240\246\232QS\001\305\030\024\030\373\212|C\'\025d\214b\245F\253\0108\311\253p\0163W\2438\002\254#T\312\325\"\363S\371{\223\"\242\t\212R\265\033\naZaZiZaZ6PR\230V\230V\232V\223m&\3326\321\2121@\024\340)\340S\200\247\201N\002\235\212\\z\320G4\204SqHE&)1F\3321F(\305(\024\340)qF+\313\305:\233\324\323\207\024\340sN\024\341\326\234[<S\201\30585\004\322\216\265(<S\224S\311\342\235o\313\223V$\351Dun3\221V\340\340U\3055:\032\225M<?5j\t{\036\206\245e\364\250\272\366\246\225\246\225\246\2244\302\264\233)|\272k-F\303\025\021\300\246\344QF(\333F\332M\264\273iB\323\302\323\200\247\001O\013F=)q\221JG4\322)1I\212L{Rb\227m&(\305\030\243\024\360)qF+\312\305-8R\232\000\251\000\300\240\236\324\243\2123J\032\236\r(5\"5H\r9\217\024\350\016\01754\215O\214\325\250\210\365\253\221\034U\244j\231Z\245V\245\316\rK\024\234\214V\222r\242\230\340\006\351H\303\212M\224\322\264\322\264\334\000ipOALh\237\322\253:\234\340\320\266\254\3434\326\262n\306\241h\335\017\"\220>:\361O\0074\360)vQ\266\235\262\224-8-8-;\003\246h\372R\201\216\224b\220\212LRm\244\305\033h\305\030\244\305\030\245\3058\n1K\212\362\214\322\212~8\245\002\236\006(&\221FNM8\2650\265&\352xzQ\'\255=^\245\022\nz\276jd \032{\216\364\364n*x\233\201W\242|u\251\325\252tz\225[4\254\330\2536\213\274\346\264\320\342\221\316\347\030\243\024\264\306 S0Z\244[|\363R\254[y\247\272\251N:\325#lY\262EZHF\334T2\302\312x\025]\243\335\324Tmh\030t\252\222@\321\036(G\365\253\n3N\tN\331@J]\264\270\243m\024\270\245\000\032B\264\233i\010\244\305*\216\264\230\240\212LQ\212]\264\340(\305\030\257%=jTZy\342\223v(\r\232w\035\351\245\361\322\243/L\336(\337\336\223\314\315H\032\246\215\207J\221\030d\324\350A\251\224`\324\200\3664\231\332\246\247\203$f\256+p*tj\2305L\217O-\232\275b\3406=kQ\024\036\225\033|\214sK\221Mg\364\241\020\267&\254EnX\360*\372[\205\030\"\234b@9\305FD=\360*\264\262 8Z\256\323m<S\326F\223\036\225(\205Xt\346\206\266\300\371j\264\266\233\263\221Td\262 \344\n\215T\241\346\247A\232\227e.\312M\264\322\264\230\243\024\270\245\333AZM\264\205i6\322\250\000\234\322b\215\264\230\243\024b\235\2121F+\3111\315H\274R\232h\031\247\022\0050\275F\315L\'4\302i\246OJE\220w\247\254\303\326\244YT09\247\254\336c\341x\253h\330\034\032\262\262|\2714\261\311\274\373\324\2238\001GsVm[\367d\032\235\0375*\271\035*d\223&\254+S\303\324\360\273\0021[6\363\344\014\365\253C\347\3523A\210t\002\221m\035\317\3355v\033=\277z\254\210\302\014\360\005C-\317e\252\31730\250\031\232\252\276\342i\2715<R\221S\213\254\034T\3510r0qW\002\006Q\300\346\230\326\201\272U9\3541\310\025I\255\3323\2208\247\'5!^)\205i\245i\245h\013K\266\224-\005i1I\266\215\264\230\306i\002\322\355\244\305&)qKE\030\257&\331\212pZk\373Sz\n\214\234\323\t4\302Oz\211\346\003\201P\371\244\236i\371\310\250\217ZQOZz\261C\221W\"\23389\347\275X3\340\343\265Ii \363y5=\311\314\253\212\263\013`}ju|T\252\364\365|\032\230MV#9\346\256F\330\034\325\353yFEn[H\205GL\325\245e\'\370sR\371\321\240\371\216\343\350)>\322\010\302.\rC$\245\270&\253\021\232M\264\322\225\023\307P\024\240)\024\354q\232\221\t\035+F\336^0\335*\364d\036\225ab\0140\302\240\237O\005IQX\3676\215\021\334\005B\215\232v\332B\264\322\264m\243m.\334Rm\244\333AZM\264\322\274\322\343\024\323\3054\232M\324n\245\315\'>\224\273I\257+lRg\216)\204z\324nj\026`*#%D\362Uwj\214\266)\313%?viA\247\006\305(z\221\037oJ\262\254\n\362y\244Y\2126Gj\320\211\314\307y\364\253L\333UO\255>9sS\253\322\371\230\245\216B^\267\355\255\013@\035O\024\004!\261V\243!:\346\256\3013\266\002\212\324\2066#.j\177\335G\313\234\232O47B\007\2654\234\367\243\024b\227\024\322\200\323\014T\337*\236\261\002)D\035\305H\250E_\266\343\025}\010\317<T\340c\336\253\\[\t\201\342\271\313\270\r\274\247\035(\214\206\034S\212\322\025\244\333N\333HG4\025\246\342\220\2121M\300\250\335\200\250\211,x\247\010\031\251\337g#\255 \217\232\221a8\247yT\206<W\224\025\036\224\323\201P\271\250\035\260*\263\022MF\306\241cQ1\250\215 5\"\276)w\322\356\247\006\247\207\251\222BF\321Ly\n\235\244V\345\236\014JE>v\373\253N\210\360*\33251\337\232\265n\200\200Ml\333]\210\343\nNG\245)\273\313p1Wm\246\014>`\010\255{f\207\031R\006:\324\263_\240\033R\253\t\313\036Nj\302>ju\346\237J\r\007\353J\r\024\340\231\247*S\300\301\251\000\366\251\020\340\325\373e\336G5\243\345qQ:\n\310\324\255<\300N+\017i\205\275\252\302:\260\340\322\221I\200)\t\024\231\024\322\342\2432\016\325\031v=\005&\347\364\246\222\347\265\013\003\261\311\253Q[\205\353S\355\013Ma\236\324\301\037=)\341\016)\014f\217.\274\204\212\211\205B\374UW\3115\013qQ\265D\325\013\na\024\323I\232\\\322\356\245\rO\rOI6\266jI\037p\007\336\266l\234yC\236\224\347\270\005\261\217\306\244\216L\324\302LSD\233\237\025~96\000\005Z\215\213\n\265o\t\221\376n\007z\272\322*\r\211NIXp\rL\256OSVcj\265\033\342\254\244\2250z]\364\273\251U\252@i\353RS\200\006\236\242\237\264\324\360\314b\"\265\355\356\026T\301\373\325#GU\346\2040<V%\345\230\311\300\254\326\264pr\271\024\236T\302\233\344\316{R\213Y\217\\\323\276\303.y\3158i\316z\223S\307`\243\250\315Y\032x#!x\2456 \034\025\250\332\311Gjg\223\216\202\220\300~\224\242\020:\322\024\002\220\250\024\224\323M\257\0365\023\036*\274\207\203U\232\241j\214\364\250\332\243\"\243\"\230E&(\305%\024\340iI\251\003.\337z\277gq\225\353\322\234\322\345\252\344-\305L[\212\215$\303\326\234\r\270\014\326\214X\002\255\tx\302\360)\312jej\231\036\254F\365e\036\247G\251\203\232xzxzP\365\"\266jd5.}\351\312\325(5\"\234\365\245#\232\275g(\215\206k\\:\272|\247\232\211\205A%\272\277QP\265\232\366\000\325w\266\307E\240D\000\345)\2050\331\002\246\t\274r)D\002\203\010ZM\341F\007J\202I\262x\246\371\343\0375D\323F\017\024\217.zTe\252\026j\214\275(9\2434\332\362\006\025\013\212\251)\346\253\275DE0\212a\024\302\264\322\264\302\264\322\264\205i\244SH\305(\245\305C<\276Z\222\016)\332d\345\263\223\326\264\201%\271\253\321>\005J_\212\213v\0335\255k\'\356\324\326\2042f\255!\251\224\323\267\372T\250MXF\305YI\005L\263\001R\254\242\236$\247\t(\022T\311%N\222T\242Z\221^\246G\251\224\346\245Q\2322T\3475b+\355\204\014\326\2147K(\031\353S\343\214\212\214\322\034S\n\003Q\030\2114\360\240S\361\212\2574\300dt\252NI\031\rU\244W\316s\232\202\\\2163LRE;y\0244\231\025\02350\265(zv\354\320My\023Uy\033\322\252?Z\205\2050\2554\2550\2554\2550\2554\212a\024\323L\"\232i\264\352\315\324$\302\220*\326\212\204\214\236\225\253\374uj3S\003\305D\347\006\264\355\030\030\226\264\242\224\001\201V#z\260\255R\241\031\353S\254\201{f\227\314$\344S\326CS\243\023S+\021\326\244\022S\303\323\203T\210\325:\275H\262T\213%X\216J\265\033U\225aMs\236\225]\301\007\"\254\332\335la\232\337\212ex\301\0074\034\032a\024\200\322\346\220\220*3(9\025J\347\'\221T\335\231V\242\016O\336\244`\r7g\025\033\014Td\323\r4\232@h\017\212R\365\344\2621\252\316j\026\031\246\225\246\025\246\221M+NX\201\004\261\305Wu\347\212\210\212a\250\3150\323\t\244\243\265f]\246\351\000\255{\030\374\250F:\325\241\367\252t5:\322\262f\255@\031\000\364\253)!\0075r)\263\336\256F\371\251\343 \036j~\033\356\324\253\220=\351y\0075<s`sN\363A\247\007\247\211)\302Z\221f\307z\224M\232\221d\251U\352x\344\305]\212^*e\222\246S\221Lz\205\362\274\325\375:\364\203\265\217\025\246\263\202z\323\314\312\007Z\210\334(\353Lk\324\025]\356\331\363\216\225\037\236Oz\212IK\036\264\306pG5\0162j@\243\034\322\021\201P\265B\324\323\315Fi\271\244&\232My[\324\014)\273x4\302*2)\244Rb\232\335*&\025\023\n\205\252\0264\302i\244\321JzV|\347\367\2035\253f\013F\t\253\241EH\270\251\227\024\375\352*\304rdd\364\246Ip\027\201V-\244\316+J90\005YG\3175b9MXY)\333\363F\352xjxz]\364y\224\tjT\224\325\210\336\255\306\331\251\324\212\263\033U\205z\231d\305!|\232q\033\326\230\243\3139\025g\355g\030\357A\271\'\2754\316i\245\311\245\016i\333\251\214i\264n\300\342\223\314\315!zal\324Li\264\323\315Fx\246\223M&\274\265\2522\264\230\3050\2550\2550\2554\212c\n\205\252\027\250\036\241ja\244\245\002\234G\025F\346>sW4\351\2066\261\300\025\246\010<\002)y\024\354\232P\244\375*O7h\300\252\345\213\311Zv\274\001Z\010\365z\006\335\305Y\300\035)\003\324\210\331\251\001\247\006\247n\244\337@|\323\3075\"\266*\302I\212\263\034\265a$\253\t-X\216J\225_4\375\325\"59\2153\275<c\024\022(\016)\300\323\267SI\246\223M&\230M4\265!4\231\2444\303Lja\024\323^`E \000R0\310\342\243+\212a\025\033\na\250\232\241j\205\352\006\250\310\246\221M\3058\np\\\324\0271\344b\250\020\361\234\212\261m~\361\261\3632kF\rAX\363W\005\302\205\310\002\241{\242N)V\\\212\236\021\223Z1\014\n\262\215\212\265\004\2705o\315\342\220?5\"\311R\211)\302J]\364\273\350\rS#f\235\272\244F\2531\275XW\251VLT\361KV\222Q\353R\t\005J\262S\367\344Rf\205\223\024\374\203M\350i\333\351w\346\215\324\322i\244\323\t\246\346\2234\271\244\242\242q\212`>\264W\231\225\246\221M\"\230\302\243\"\243e\250\332\242aP\260\250\\T,)\204SH\244\333J\026\244\013\212\216E\315Wh\001\250M\267=(0\024<U\310\203\005\000\232q\214\346\247\211=j\324\177)\253\321\276EN\246\246F\305N$\247\207\315<=J\257\305/\231N\363)C\323\203\324\321\2658\2775,oVQ\252Q%<IR\307.OZ\264\254}jt~9\247\2111R\ti\342L\364\245\316i\341\360(\363)\013\322\206\247n\244\335M&\220\232J(\244\006\202qMnj\"0h\2578+M+L+Q\260\250\330sLaP0\250\330TL*\026\025\021ZaZn\3326\323\325i\032\243\306z\323X`PW\214\323H\311\025\"\234S\267\323\225\352\302\034\325\250\016*\342\232\231N)\341\205<0\247\253T\201\2517S\267\323\325\362je\347\255I\274(\244W\311\251\325\361R\254\2250~)C\346\244W*\302\256\307>@\251\204\331\2453zS\322\\\324\352\365 z]\364\240\320[\024\t1N\017\232]\324\205\251\273\251A\247f\226\233Fr)\244\342\230Ni3^zV\232V\243+Q\262\324L*6\025\023-D\302\243aP\262\324ei\214\264\335\264m\243\024\322\271\244\333\212\215\305+\034\001L\003\234\324\200R\021J\2654mW\"j\266\207\"\234d\305\002\\\324\252\365a\017\024\354\321\232P\325\"6*p\331\034SY\215*\275J$\251VJ\235d\342\236\262`\322\274\26543\347\203S\211H\247\2113S#\342\246\022T\202Zw\235@\233=\351|\312O2\234\262{\323\304\224o\245\017OSO\006\2279\244&\243\'\232B\324\302i7W\020R\243+Le\250\231j&Z\211\226\243e\250\231j&Z\211\226\230V\230R\233\262\223m&\3326\323J\324,\274\323\030f\200\265 \024\021J\0059\001\'\212\267\020\351V\323\245)\024\321\301\251\220\324\352\324\360\324\354\321\232p\353\305N\206\225\31538\247\253T\252\325*\275J\0374\355\331\251\"85d7\024\252\325:\311\305<IN\022\342\227\315\245\363h\023R\211sR\007\247\t)\341\251\352jEj\2245.\3523Q\263S\t\315F\317\212a\222\271r\225\033%D\313Q2\324L\265\023-D\313Q2\324l\265\031Jc-FV\232V\233\266\232E&)\010\250XS6\322\355\247SM\024\36485n6\351V\220\346\224\232eH\271\251T\324\200\323\301\247\212p5 <SK\346\220\032x5*\324\200\324\210jPjDlT\241\251\301\252P\334S\303S\267SK\342\215\364\241\251\352\3252\275<75\"\236j`i\331\251\025\251wQ\277\212\214\266i\204\342\242w\250KV;-DW\332\243t\301\250\231*\026Z\211\226\242d\250\331*&JaJ\215\222\230R\232\313L)L+M+L\"\230V\233\266\223\024\230\246\221F)EH\214E[\211\315N\334\212h\251T\324\213\315H\005H\005;\024S\351\273y\247\001O\002\245Zx\251\024\342\244\006\2245J\255N\335\315L\215\232x4\271\246\226\244\335N\rR+T\212j\302sS(\342\234\033\265\005\251U\351\333\3517R\347\271\250\335\352\026j\210\265Qd\250\331j6N9\353P\262TL\225\023%F\311Q2Tl\225\031J\215\222\230R\243e\246\262\324ei\205i\245i\205i\214\2704\322)\204b\214Rb\235\212U\025:\034T\313\'cO\034\324\212*U\251T\324\200\322\356\245\3158\0323NZx\247\347\024\241\251\301\351|\312<\312\225$\251\224\346\246V\305H\032\224\2654\323i\300\323\301\251\243oZ\266\203\214\324\201\261N\006\224\234\324d\340\323\203f\234\010\035i\035\270\342\240g\250\331\251\205\251\205j\"\231\2462\324,\225\033%D\311Q\262TL\225\033%F\311Q2sL)Q\262S\nTei\205i\245i\205*7^j2\264\322)1F(\305(\025\"\324\212\271\2531\245K\267\024\n\220\032p4\341N\006\227u\004\320\254jA%.\362i\273\311\247\253\023R\014\232xZ\225\026\246\034S\267b\234\257\315)j\004\225 `i\331\245\006\244C\212\266\217\305K\270S|\316iZN8\244\031\357J[h\250\314\246\2173\212c5FZ\2235;&j6J\215\222\242d\250\312Tl\225\023%F\311Q\224\250\331*\"\225\031J\215\222\230\313Q\262S\nS\nSJ\324L\265\033%1\226\223m&\332\n\320\026\236\005J\202\254\245I\232\000\3158\nv)\302\235M4\240\323\205\004\322\203N\006\236\rH\246\245SN\335\212Q%;vi\312i\371\315\000T\200T\213\315?\024\341R\253\340T\201\263N\343\326\234\010\245/\212k8\"\240\335\315.\352Bi\231\2435\242R\243)Q\262TL\224\306J\210\245F\311Q\262Tl\225\033%BR\243e\250\331*2\224\302\225\031ZiZ\214\2550\245F\311Q\272\323v\321\266\215\264\233)\301jE\030\251\024\342\237\232@\3305:\020\325&(\002\226\212a8\243u(4\340i\324\3655\"\324\253N\306h\332i\301H\247\250\247\347\024\241\252e\251\007\255H\2434\360\224\275\005*\265\005\350\3631A\223\"\233\272\233\236i\331\244\3154\320\rm\024\250\312Tl\225\031J\215\222\243d\250\331*6J\211\226\242aQ2Tl\265\033-D\313Q0\246\021L\"\233\266\232R\243)Q\262S6Rm\245\333F\332\002\323\202\323\302\323\266\322\025\247\247\006\254\216E8\n\030T\014H4\204\346\201\326\234*E\024\354S\324T\310*P8\243<\324\213Rb\214SM*\324\240\342\244\00752\n\224P\335\351\203\212k5F\033&\235\232M\324f\234\0334\271\240\322W\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\006\"IDATx^\355\335\333\266\342(\020\000\320\263\234\377\377\344q\315x;G1\367\020\002\324\336Om4\tP\005\001\264\273\177~h\307%=\000\000\000\000\004`G\000\000\000\310\315:\003\000\000\000\000\000\000\000\000\000\000\000\000\000\000\032\345/\013\003\000\000\000\000\324\315>.\000\000\000DdG\000\000\000\000\000\000\000\000\240]~\373\001\000\000\254b\021\001\000\000\000q\\\323\003\204\364\177\036\\\345\002\313\330=\214N\006\000\000t\3114\017\000\242\2623\314\274\213\311bl\022\000b2C\210I\334\001\000\000bJ\326\203v\004[\267r\201\177\013\370\312S\250\232h\002\000\000\000@\\\266\370\001\000X\301\3641\256{\354%\000\000\000\324+\347\277\362\221\363Z\344\347\267\337\214\320s\203\223\000\020\227\376\017\000\221\331%\000\350\226!\036\200\233\322\317\203\322\367#\221\006 }\235\231\315e\000\000\000\200\340\016\336\177\002\000\000\340 \326s\237\242\374\000\"S\3343]\006\000\000\000h\202\235\000\000\000\200p\236\337\241G\371*\2357\266\001\240\t\272*\000\333\230\341C([\273\374\326\363h\335\353?\233\277Zo\004\'\001\242\223\001\301I\200,\314\246\000h\215g\027\000\000\000@\313\354\356\000\000\000S\374(,\2364\346\351k\000\000B0\r\004\010\353\342+\344\310D?8\t\000\000\020\203\255\277\350~3\300\022\000\000\272c\246\307:2\006 \212\333\026\200Q\237\317\034\2601\004\000\315\362\030\007V\353vE\330m\305 \247\367\216\242\323\304!\326\335\261\n\000\000`-s\310\270\304\036\"3\002\000\000DgF\030\214\357\205\203\223\000\020\217\'=\000\360\306\324\240m\342\007\000\360db\024\234\004\240i\333\276\255\333v\326\235\036\323\236#b\266#\205\340|Gt\n\000\200\354rNZ\314\340\033\2645\001\266\2367\355\230\253RJ\215C\200\234\002hC\215\317\020\312\362\314\216N\006\004\367L\000y\020L\032\360\3645\264\3004\026\200\332\205\231c}U\364\353\000\347\021\014\010n|\335\264kx\030\277,\237\216j\251\r\341\033:\345\250\342\0015\032\032\005&\031\"j\366\027\316j\343\264:\343\350\313\241\tp\350\305Y\244\332\241\007z\226\253\343\355\271\316eh\004\23696\3646m\022\313S\355\351\2720+C\202e\270D.F\253\315\036Q\254(\226\034+\355+[B\177;g\313yT\350-\220b\332\274\264{\007P.k\017j\334W\005\376*\222\365F\213/\366\371\301\301-\200\345\336\316.\027\240\266\034\325.k\003\267\366\363\017\327G\361/\267?\354\314\025\010\355\264\356\263q\010z\225\367\275\334\317\361\000\210\313(P7\361\251J\301\'\377\\\3443\025e\3566a\2156L\246\206_\351\25589\013\220\363Zg\030\215\322N\023\355r9\352\236s&\312DnU5\366\2430\037\313\267\267?\347UU\305\3330\030\214\301\203\313\315\237\2763P;O\347\333|\314\026\273G\347~\275#{\375d\016\330!Z-\177\213\215E(\377\235\330\357\370\250<\362\341\370\373\260\322XG\205\355^Yu\371\353\363\217I\301?\333\307\200\3475G\022\366\357pz\203\221\023\342\032i\220\221\303\204\223\366\240\212\205N\332\232\3434X\266\301\203\031\035}\375\316u\333|\335V\354t#-;r\370\347\347\237\364@)\243%\"\261\252\245^_*\255:\351h\203\205\031<\310\200\365-\025z\026\326\201\201\345\372*kN\272\337+G\302\244\327x\177\235\276\307\264{\000s5Z\235?\323/V\250\341\316P\354\366\333\014\027z\205\327\267n\225\327\223#}\007\377\324/c/i\201\316,\014P\232\036\177\256\357\'B\020\022o\204\206i\317\242\230]\236+\277\267\017?W\202\'\016\002\023\267\236x\213E\021\037uo\332\312\332w\2668\373\252\314\237\340-9\233i\255Z\\\261SW\375;,,\366X;,<=\214z\333cc\311\346N\273\275?\367\231V\215%}O\216\036\267\016\276|niq\353\374b\'\217\216\253\266]\232\000[i\3346=\306\303g\364\326\006\361\231<\327\333E\326\236{\214\301ei\372z\314\327\227I\237n\225\374\374\300\324\253Pj\257\372\\\371\026\217\202\237\027\372=m\361\371\375\231k\332>\254\255\345\332\317\323\210\300=}Xo\r\322[}8\214T\201\272\035:\021\233\272\370\324{\034cn@\236{\177\316i\273\243\005\357[\360V\345$\201\337\233\007\257m\263\304\3201j$Rls\037\036\273\034#\013i\275\353\365\036\373\326\343\303N\317\004\350)\017~\353\322S\245\250\305\340b \210\337\252\367\376\\<\303\231y\2253\251\037\227\272\234[\241\r\032+.p4\203B\007\0041<)\000\254d\235\373-c\233\344\\v\316\333R\360\353\343\311\361*g\321\362\226\266\265r[\317k\307\226\314\351[\3771gJ\323\361o\272\360\203\014P\253\364\227\000\265Jfx\265\344iw\t\320]\205*QK\302Nk\243\2244\301P\302\"i\242\224\032\2052\335\'\323ez\244i\356\322\004\207\030\226\016\000\317\317\351(\301I\000\000ZP\366\207\007wK\247T\243N+\362X\311G\217\217\275\001\264\245\374\250CU$\000\363<\362\001\272t\375\367\371C\370e\032~\032\254\250e0\227[\323\274\375\017\022\327\206\243\014l\363\335\355\215\231\000\224\366\3754\002\330\302L\026\240\010\303-\000\205y\364\000\000\204\347\357-\001\000,c\332\004\020\234\357T\000\000\370f\273\200A\022\003\000\200\305\354=\003\000\000\000\000\361\330\031\355\204@\002\000\000\300\371\254\317\001\000\200I\345\376\322\263\345IY\325\265wu\005\nF\373\003\000PX\271\325f\353\252\233\254WW \000\000\000\000\000\000:\344\013E\000\000\000\000\000\250\312\177]O\370\366;\244\233\330\000\000\000\000IEND\256B`\202"
diff --git a/core/res/geoid_height_map_assets/tile-b.textpb b/core/res/geoid_height_map_assets/tile-b.textpb
new file mode 100644
index 0000000..b04a194
--- /dev/null
+++ b/core/res/geoid_height_map_assets/tile-b.textpb
@@ -0,0 +1,3 @@
+tile_key: "b"
+byte_jpeg: "\377\330\377\340\000\020JFIF\000\001\002\000\000\001\000\001\000\000\377\333\000C\000\004\003\003\004\003\003\004\004\003\004\005\004\004\005\006\n\007\006\006\006\006\r\t\n\010\n\017\r\020\020\017\r\017\016\021\023\030\024\021\022\027\022\016\017\025\034\025\027\031\031\033\033\033\020\024\035\037\035\032\037\030\032\033\032\377\300\000\013\010\002\000\002\000\001\001\021\000\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\332\000\010\001\001\000\000?\000\310\315-\004\346\212(\245\315-\024\003O\006\234\r;4\273\251sFh\335F\352v\374\014S7SKRn\243}(jP\364\340\364\341%<I\357N\022R\371\264\276m;\315\367\245\022S\204\224\3572\227\314\243\314\243\314\251c\2235)l\212Bx\254\373\221\345\266}j5|\323\213SKS\013Rn\240\265\033\2517Q\272\233\272\224\034\325\210\370\024\354\322n\244-\201Q4\300TfqQ\231\3523=!\270\250\332\350Uv\271$\325\243I\232\\\321\2323Fih\315\024S\201\247\003K\232PisK\232Bizu\246\226\246\226\244-I\272\223u.\3527\322\357\245\017K\346R\371\264\242JQ%H\254M<\023N\334iw\032]\364n\243}H\222b\247\022\344u\240\311\216\365\005\313\007_\245T\007\322\235\272\230Z\232Z\233\272\227u\033\2517Rn\240\232\222>\265eM\014@\250ZP;\325y.3\300\250\014\204\323K\323\013\323\013\324l\365\0235D\317\212\326\242\212(\242\2123K\232L\322\203N\315\031\245\315.isK\232\032L\214S7SKRf\223u!jM\324n\243}\033\350\337F\372pzp~j\302?\025&\372M\364o\245\337K\272\223u(z\221d\247\027\310\252\357)\246+f\224\265F[\024\205\2517Q\272\214\322n\245\rAl\232\2323\201Ry\230\250%\237\035\rViI\357Q\226\315&i\t\2463Tl\325\031j\215\232\242c[tRf\214\321\232J(\315.h\315-\031\245\315\031\245\317\275(4n\3054\2654\2654\265&\3527SKRn\244\335I\272\215\324\233\350\337OS\232v\354S\326lT\202l\322\371\236\364\242J\220IK\276\215\324\233\251\301\351\346L\n\202Bv\223H\207\212R\325\03357u\033\250\335F\352M\324\273\250\335R\tp)\217)\250K\023IHM4\232i4\302\325\033\032\214\232\214\232a5\273A\246\321E&h\315%\024\271\2434\264f\2274f\214\321\232ijajn\352M\324\233\251\013R\026\246\226\243u!jizM\364\365\233\024\246\\\322y\224\242Zp\232\244YI\357R\254\224\361%8=.\352pj\035\270\244V\334\244Ss\216)\245\251\205\251\273\250\335J\032\215\324u\245\346\212Bi\271\244\315!4\204\323I\246\023Q\223Q\261\250\311\246\023L&\272\n\r6\212L\322QHh\315\031\244\315(4\271\2434\243\212L\346\202i\205\251\205\251\245\2517f\220\2650\265&\352izM\364\205\351\205\351\273\350\337K\346R\371\224\273\351C\323\226J\235e\315J\036\236\036\234\036\234$\244y:sJ\222\000iX\367\024\302\324\302i\271\245\315.iG4\361J[\212\215\232\2234\204\322f\222\220\232a5\0314\302j64\302j2i\244\327EHi(\246\321E!4\334\321\232L\322\346\214\322\346\202\324\200\322\026\2463S\t\246\026\243<SKS\013SKSKSK\323\013\323w\322\027\244\337F\372<\312]\364\340\364\242J\231%\305L\262\324\213%?\314\364\240=#>M9^\244W\310\347\2654\2674\322h\315-.}i\333\370\342\223u\033\250\244\315!4R\023L&\230MFM0\232a5\0314\323L&\272J\017Jm\006\233E!\246\232JL\321\2323Fh\315\004\320N)\244\324d\323I\3053=\350-Q\226\246\026\246\026\246\226\246\026\246\226\246\026\246\227\244\337I\276\200\364\355\364\273\351|\312Q-H\263\221\336\247I\263R\254\224\340\364o\346\244W\247\253\340\020)7R\356\245\315.h\006\2274f\224Q\232L\321\326\202i\244\323\t\250\311\246\023L&\230M0\232a\246\232\351h\2444\224\323E!\244\246\322\032J)3@4w\244&\232M0\232c\036\324\204\323\t\246\023Q\263Te\251\205\251\205\251\205\351\245\351\205\351\273\350\337I\276\227}.\372<\312]\364\241\352T\223\336\254$\324\363/\245*\311\236jUz\2326\371I\245\315(4\340is\315.is\3158R\346\220\232\000\240\232i4\302i\204\323\t\246\023L&\230i\246\230M0\327OE\024\204b\222\220\212i\244\244\"\230h\244\244<P\017\024\236\364\204\323I\246\023L\246\223L&\243cQ\226\250\331\2526j\214\2650\2650\2750\265&\372iz7\322o\245\337F\372]\364\340\365\"\275H\257R\253f\246CS\003R\253\343\212x4\360isN\006\2279\247\201KFi:\322\346\2234\322i\204\323I\246\032a4\303M4\323Q\232i\353]0\245\242\212LRR\021\212i\246\232a\246\232)\t\243\266(4\323LcQ\223\232Bj64\302j65\023\032\214\232\211\232\243-L-Q\223M&\220\232ijM\324\233\250\335F\372]\324\360\325\"\265J\246\246SS\306\325:\234\014\232\"$\2615`\032p4\360iA\315H\264\354\322\023Fh\315\031\244&\232M4\323\t\246\232a\246\232i\246\032a\246\036\365\324QK\2121IHFi\r4\212i\024\303L4\206\232i\302\203L&\243<\323I\250\330\323\t\250\330\324Lj&5\0335D\306\243&\243&\232M0\232B\324\322\324\334\322f\214\322\346\224\032\221MH\246\245Z\231jelu5 \334\374t\025:|\243\025(4\360qN\0074\360i\333\251wQ\272\2234\271\2434f\232M4\232i\246\232i\246\236\264\303\326\232i\206\232k\247\245\002\226\212)1IM\"\230E0\212a\246\232h\031j\220\323\032\243cQ\261\246\026\250\313Tl\325\033\032\211\215F\306\242f\250\311\250\311\3150\232c\032a4\322i\244\322\023I\232)sN\006\244PML\213\212\235EJ\242\246LT\240\323\303T\212qO\rN\rN\rJ\032\215\364\273\251sJ\r.h\315%6\233M4\206\232i\246\230i\244S\ru\000R\321E\024Rb\220\212a\024\302)\204S\010\305\"\214sJM0\232\211\215BO4\306j\214\265F\315Q3Tl\325\0335DZ\243-L-L-L-L-L&\220\232Bh\315\002\236\005H\270\025 j\221Z\245V\251U\252Uj\2205H\255\212v\372P\364\340\364\340\324\355\364\241\251\300\323\201\245\006\235\2323HM%%!\246\232CM4\303M4\303]E\024\nv)\010\243\024\224\206\232i\204S\010\250\330Rt\246\223Q\261\250]\252\"j2\325\0235F\315Q3Tl\325\0235F\317Q3\324e\351\205\351\245\351\245\251\273\2517Q\270R\027\002\200\364\242J\220=H\255R\253T\252\325*\275<IR\007\305;\314\247\007\247\207\247\007\247\207\247\006\247\203O\006\234\r8\032vh\315\031\243\024b\222\232E!\024\322)\204S\r4\212\3521F)h\242\212)\246\232i\244S\010\246\021\315Fj65\023\032\205\2175\023\032\205\232\242f\250\231\2526j\211\232\242g\250Y\3526z\214\2750\2754\2754\2757}4\312\0057\315\317J\003\323\203S\203T\201\252Uj\221Z\244\017R\007\247\207\247\211)\301\351\341\351\341\351\341\252@\324\365j\221MH\r<\032p4\340ii@\245\243\024b\232E!\024\302)\244S\010\246\221]=\024R\342\222\212(\246\221L4\302*6\3435\0215\033T.x\250\030\324.j\0265\023\032\211\232\242f\250Y\252\026j\211\232\243f\250\313S\013Te\351\205\351\215%G\270\232z\265(l\323\303S\324\324\201\275\352Ea\232x\223\322\236\257R+\032P\3478\024\375\304S\225\352A%H\032\234\034\346\247V\251\024\324\252j@jAO\024\361N\002\234\005(\024\270\244\"\220\212i\024\322)\204SH\246\021]-\024\242\226\212CA4\224\207\2554\323\010\250$<\324MQ1\250\\\324.j\0075\003\032\205\215D\306\242cP\261\250\035\252&lTL\325\031jaz\215\232\243g\246\023M\3159I\251\001\247\255H\r.\352p$\324\203#\255J\204S\214\224\345\222\245\022\003\326\234\007\245<T\252i\370\364\251\025\252u5*\232\225jQR\001O\002\236\0058\n\\R\342\214R\021M\"\232V\232E0\2554\255t4\240R\321E!4\224\231\244\246\223McU\334\345\252&\250X\324\016y\250\234\325w5\003\032\205\215D\306\242f\250]\252\273\265B\306\242cQ\026\246\223Q\226\246\023M\335\353H95*\2169\247\n\220t\251\025sO\333\266\233\346`\361O\014M.\354Q\27352\014\324\350\265a\022\244\333\353J\023\322\237\202)\313\326\247Z\235*u\251\024T\240S\300\247\201N\002\234\005.(\333F\332iZiZiZa\024\322\265\277E\024\334\321Hh&\222\220\232i\2461\252\354y\250\330\324,j\274\206\241sP5B\365\003\324Lj\0275\003\232\201\215D\306\241cQ\023M&\230MFM&iG\006\246S\232pZ\225H\305L\224\366\\\216*\002\2304\340\330\244\335\232\221Fj\304B\256\304\231\251\200\305<.iB\363R\252\344sM)\203R\240\342\246Z\235*e\025*\212\220\nx\024\340)\300S\202\322\355\244\333HV\232V\232V\230V\230V\266\350\240\323h\246\232)3Hi\244\324NqP1\344\324lj&\252\362T-P\265D\302\241aP\270\250\034Uw\025\003T\017Q1\250\311\246\032a\024\302(\301\245\305J\275*E\346\236\006\rJ\247\025(a\212\211\332\241\316M=x\251\343\346\254\307\305\\\215\300\025 l\232\260\203\212SOC\232\221\223\214\322(\305L\242\246AV\024T\252*@\264\360\264\360\264\360\264\340\264\273i\n\322m\246\225\246\225\246\025\246\025\255|RQHz\322\032m!\244\2444\302j\'<T\016pj65\023\032\205\352&\250Z\243aQ0\250\\Uw\025\003\212\256\342\253\270\250\231j\"\264\335\264\205i\002d\323\304x\245\362\351v\323\325qO\333K\332\230X\212o&\234\006)GZ\265\020\251~\225b%&\255\"\342\246\007\002\234\243uXH\352m\234Sv\324\212\265*\n\260\202\246U\251B\323\302\323\302\323\302\323\266\321\266\215\264\233i\245i\245i\205i\205kO\024\204RR\032Jm!\244\246\232a\250^\241z\214\324MQ\265B\325\033TMQ\232\205\205@\342\240qU\335*&J\215\2435\023Di\236Y\357Hc4\2011N\000\367\245\002\224\nz\2558\256ivf\223\312\2441\342\230i\312\271\2531-YT\315Z\215@\251@\251V2j\314Q\342\255\004\000P\027\232\177\223\306E*\307O\013\212\231\026\254*\324\241i\341i\341i\341iv\322\355\243m&\332B\264\322\265\031ZiZ\277M\2444\230\246\221Hi\246\222\230i\215Q=B\342\242j\214\324mQ5F\325\023\n\215\205D\374\032\205\306j\022\264\206,\216j3\020\035\005D\321f\243x\261P\025\305&\332B\224\233iB\322\204\247\205\247\205\245\002\206\342\241v\250\361\223S\306\265:\220*d5e\rX\214d\325\330\323\212\231V\245U\342\227o5<c\326\234W\024\201jdZ\260\253R\205\247\205\247\205\247\205\247m\243m\033i6\322m\246\225\246\025\246\025\253dRSM6\220\322SH\246\323Z\243j\215\273\324M\322\242aQ\021Q\260\250\330TL)\214*&\025\013\324{\t\240\246)\205i\276]4\245E\"qUY2i\2451M\305&\332]\264\241i\301i\333i\002\320W\212\201\322\232\253S\242\324\351\0215f8qRc\025<\0035\241\030\300\251\325sS\250\247\204\315H\251N\307\024\212\2715aW\0252\255J\253O\013O\013O\013N\013F\3326\322m\244\333HV\230V\243e\251\373\320E0\212B)\010\246\221M\"\232E4\212c\n\215\205B\325\021\250\330TdS\010\250\330TMQ0\250\266n5(@\242\241q\223L\331K\266\242q\212\201\305W\"\243aL\331K\266\227m(Z~\332\002\323\212TL1Q\260\315\013\0375f8\352\322\000*a\216\324\205sV-\327\006\264\020T\350*U\025:\255<\n\220G\271iV<S\302\346\246E\251@\247\252\323\302\323\302\323\266\321\266\215\264\233i6\322\025\246\025\250\312\324\204z\322\032i\244\246\342\220\212n)\244SM1\252&\025\013\n\215\205FEFE0\212\215\252&\025\031\024*c\232k\nn\314\320S\024\302*\027\025\003\255B\313Q\225\244\333I\266\234\022\227e<&i\342*y\217\212\201\342\246\010\252E\212\246T\247\2044\365CR\355\251#\030\253h\3252\275Z\213\232\265\032\346\246\020\232\231#\300\240\2474\241qR\001R*\346\245\013R\005\247\005\247b\227m&\3326\322m\246\225\246\025\2462\320E4\212a\024\224Si\244b\220\361Q\232cTl*6\025\023\n\214\212\214\212c\n\210\212\215\205\"\246\346\251\0311\232\204\2574b\230\302\243e\250Yj&Z\211\226\243)I\345\322\354\245\tR\2549\251\026\034S\366`S\010\244\362\363M1\342\224%H\221\022j\312C\221R\255\267\265I\366ojQm\216\324\361\021\251\022#\232\275\024\\\n\273\004\\\325\321\030\305/\227Ha$dTe1NU\251\225jP\264\365ZxZv\337Z]\264\233h\333HV\232V\232V\243+Qb\220\323H\244\246\323i\r4\323M0\212\215\205F\302\242aQ\260\246\021Q\260\250\310\246\025\247F\270\247\025\250\231)\230\2468\250\215F\302\243+\232\215\222\233\266\223m(\214\232zE\315ZH\270\240\250\024\322*\"\274\323\325i\nd\324\211\025N\221b\254\307\035YH\200\251\2260jO \021\322\233\366nzT\211m\203\322\255$X\025*\361S\246MJ\005H\203\"\242t\346\220%J\253R\252\324\201i\341iB\321\266\215\264m\244+M+L+L+U\351\010\246\221M\"\232i\246\232i\206\233F)\214*&\025\031\024\306\025\031Z\215\226\243\"\2435\"/\002\244\333Q\262\324%i\214\265\036\312\215\222\231\262\220\2453\313\245\021T\213\020\251\004x\247\355\2462\324dSv\344\324\251\036jA\016jd\207\0252\303S\244X\251B\032\22649\253h\234S\304U:CNh\261Dp\222j\332\333\340t\250\331pjh\327\212\215\226\220/5\"\255L\253R\005\245\013K\266\227m\033h\333M+M+M+L+T\361F)\244SH\246\021M\"\223\024\326Zn)\010\2460\250\310\2460\250\312\323Yx\315D\302\243e\250\314D\236\265(B)\300R2\361P\025\250\331j<Rl\315!JB\224\337.\234#\245\013\212~))\215Q\021\232UJ\263\032U\224\2175*\305Vc\213\332\246\021S\2045*E\212\235R\245H\352\312G\201N\362\362jx\240\002\244u\300\252\254\274\323\324`R\025\246\355\346\244U\251\225j@\264\241iv\321\266\227m&\332B\264\322\264\302\264\302\265G\024b\220\212i\024\302)\244SqHE0\322c\212a\024\302*2)\245i\2733Q\025\244\t\223La\264\342\244\333\3057o42\361P\225\246\024\315\'\223I\263\024\306Z\214\212P)M%!\246Q\214\322\210\352E\212\254G\035ZH\352\314qf\255$5*\303O\021R\210\361R\254u2&*`\265\"\'5aW\002\230\353\232\204\245.\332M\264l\247\252\324\212\265 Zv\3326\322\355\243m!ZB\264\322\264\302)\205k?\024b\220\323H\246\021M\"\233\212\010\250\310\244\3054\212\214\2557\024\322)\204SJ\323v\323\014yjy\030\030\246\201JW\212\211\226\230\026\203\214TLj6\250\361F1H\0015 N)\254\265\021\030\245Q\232\260\221\346\254$5:\307\212\235#\315[\212:\270\221\324\242:]\224\361\030\247*T\241)\352\2252GRc\024\326Zf\312M\224l\243m8-=V\236\005;\024\273h\305\030\244\333HV\232V\230E0\255f\342\222\220\212i\031\244\"\230E7\024\204S\010\243\024\302)\245j2)1HV\233\266\233\266\225S\223H\313\3157m\005j6Zc-@\347\025\021\250\230\322\016i\304S\220\n\221\230\001P3f\230FjX\243\315\\\216<U\224Z\231S5f8\252\334q\212\262\211S\254|S\035v\322)\251\000\251TT\250\234\325\205N)\n\342\220\255&\332n\332\002\363K\262\227e(Z\220\n]\264\270\243m.\332B\264\322\264\322\264\302)\214\265\225\2121I\212LSH\246\221M\"\223\024\322\264\335\264\204S\010\246\025\244\333I\266\223m7o5 L\niJn\312\nT.\2705\033t\252\3169\250XTei1K\214\323\225M)\214\232A\001\251\004\0252E\212\262\211R\205\305K\020\346\256 \342\254GVR\246\007\002\243\220f\242PA\253(3S\252\324\310\265:\216(\"\231\266\214S@\245T\251\004y\247\210\263M1b\232\026\234\0058-.\3326\322\025\244+L+L\"\243aYX\346\223\024\230\244\"\223\024\322)\245i6\322b\232V\232V\232V\233\266\215\264l\246\224\244\331\315H\023\"\232\311HR\233\266\232\321f\252J\230\252\314*\026Zi\025\0369\251\2213R\252\n\235c\024\024\024\004\024\270\305I\035ZT\310\247*`\325\210\305[\215ju\247\n~\320EF\311\203O\214\342\254\241\251T\324\300\321\232\\f\232\374p)\025x\247\252\340\324\312\265(Z\0313P\262`\322\005\311\245\000\347\024\360\264m\244\333M+L+L+Q\262\326F(\305&)1HE!\024\233i\245i\245i\010\246\225\246\225\243m\033)Lt\335\224\276VjA\036\0055\243\246\224\250\366sJW\002\251N:\3257Z\201\205&\334\322yx4\365\030\251\024f\246Piv\223K\214R\355\315*\2575z\001\220*\307\227OH\361V\024b\236)\342\2363K\264\232@\270\251V\244S\315YS\221F)\340`SB\344\344\324\212\264\355\265*\212\224\n]\264\307L\212\213f)\3120y\247\355\346\215\264\205i\205i\205i\204Tl+\037\024b\223\024\230\366\244\305\033i6\322\025\246\225\246\225\244+I\262\200\224\355\224\273)\273*E\217\212pJC\036j6\212\231\345\342\243\227\201Y\362\214\232\254\353P\224\247*P\311L\3075*T\312E8\221\212i\346\225EJ\211\223W\240\217\025mR\244\013\212x\024\241jEL\324\311\0259\223\024\300\2315\"\305S,5*\304E=c\251V>\016j=\2704\365\024\375\264\345Z\231V\237\267\212M\264\315\234\323\n\342\244\333\362\203AZaZiZaZ\215\205F\302\261\261F)\270\243\024\230\245\305!\024\230\244\"\232V\223m\033iBS\202\322\354\245X\362jA\035;\313\243\313\250\335@\252\322\034UII5U\320\232\201\2434\317.\232W\024\021\221L\331\315<Fi\353\031\247\2244\251\021&\254,<T\311\026\rZE\000T\302\237R*\323\302T\350\2252\256)\031sO\216,\324\342*\262\221\002)\306,Rm\305!<`S6f\234\027\024\340)\300T\250*R\0061M\333J\0274yt\246<-0\212iZaZ\215\205F\302\243aX\300Q\212B)\247\212i4\224s\351Hr(\006\227\031\245\331J\026\234\022\227e8%H\261\324\201)\257\305B\317P;f\240u\315B\321\324m\025B\321\324f:\212H\361L\013\232<\263\232z\256*E\025(\214R\205\000\324\212EJ\274\324\312*E\025*\214\325\204Z\224-J\242\236\0058.jdZ\235W5b4\342\225\226\230\313P\343$\342\236\005.)B\322\343\024\365\342\2369\247\342\225EH\026\224\247\025\013/&\230E1\205F\302\242aQ\262\3268Z1Q\265\'\226Z\244X\t\352)\342\000)Lb\2436\340\3645\004\260\2249\024\211\351S*\346\237\262\224%8%8GS,t\245p*\274\2435]\226\241\220\005\025_\314\346\224\020z\320\312*\026Ni\236^j)b\342\253\252`\324\342,\212C\r\013\021\025(\030\024\335\244\323\225\rX\215\rN\026\244QS\"\325\205Z\225V\245T\251\002S\325*eJ\231R\254\"\361C\n\215\207\025\010\0304\354Q\212p\024\340\264\354S\220sR\020\0059EH\203&\236W\025\023\256~\265\t\025\033\n\211\205F\302\243aX\324c4\242.y\251\200P0\005\033I\351A\030\035j2i\t\305!\001\3075\031\267\364\247F9\301\353S\005\247\205\245\331\212@~lU\205Zd\202\2532\346\242\223\n*\224\274\325r\244\032L\322\026\"\243g4$\274\363S\262\007Z\254a\301\247\205\300\247*\344\324\242.)\246,R\010\375\252d\216\246\010\005;mH\261\346\247H\352\302\307\305=W\025:-H\022\235\214T\211S\245Y@1Lu\246\021\305BW\232\\R\355\245\003\024\360)H\342\235\030\317J~9\245\305H\215\203R1\365\2460\250\030sQ\260\250\330TL*6\025\214\027\361\251Q=i\342<\363I\267\007\245\014\307\030\250Y\252\"rh\353R\'\002\244\002\232@\317J\225E<\n\\S#\\\271\253J\274Tn*\007\030\025J^M@V\242t\250Yi\204TL\264\314\02552\271\3059>c\203S\371y\024\320\2305f5\004S\214Y\240AO\021b\236#\245\021\324\250\230\251\221j\302\247\024\204`\324\321\221R\323s\223S\306\001\024\376\2254oRu\246?J\213\024\001K\212\\P)\325,k\201Hz\323\2513\203S!\336>\224\036\265\033\016j&\025\023\n\215\205B\302\263\025*M\240\n:\212a5\033\232\205\2174\312z\322\223\216i\301\370\342\234\274\324\252\265(ZR\274S!\341\315Y\003\212\216A\200MRv\'5\013.j\"\265\023\255DV\243d\250\212\323\nf\200\270\247\257\006\256Dw\n$\\sI\023`\342\257F\273\205K\260\n<\274\323\204F\234\"5\"\305O\n\027\255J\035@\250$q\232X\344\251\374\314\212n\376j\304r\020)\373\362jd\247\356\244\334qL\357O\024\242\234\005.)@\251\001\3055\251i\010\247+\025\351S*\356\346\221\222\242d\342\240qP\260\250\330U\000\270\240\340Td\322\032\205\315B{\323i\340P\307\212E<U\204\025:\212~8\245\347\024\221\240\337\232\230\374\242\243s\221U\0359\246\024\250\331*&Z\210\255F\313P\262\3236\322\354\243mM\017\006\255\024\334\265\017\225\264\325\230\344\332*_754L;\325\215\353@\221hi@\351P<\271\351M\014\306\234\020\236\265*\307\212\220%<GR\252\201\326\234\000\365\251\024\343\245J\017\024\204\322\nx\247S\200\247b\227\024\264\032@9\305-8\n\236#\306)\314*\'\025Y\305B\302\242aT\rF\306\243cLg\342\241&\232i@\247\342\232\343\"\210\200<U\225\\qR\255<\032ZU\340\346\234[ \324f\230\302\242e\250\231j&Z\210\212c-D\311L+I\217j6\323\227\203\305Z\211\370\301\251\366\006\246\371T\341\035H\024\216\224\034\322\000\324\340\204\323\304t\274\npjQ!\025\"K\353R\356\310\310\245\334M9sS(\342\236)\373sJ\006)@\247\201N\002\234\005;\024b\227\024\322)@\247/\314i\377\000t\361O\316E1\371\025Y\352\026\250\232\263\230\324Lj\026j\214\234\323M&9\247\216\00586i\257\351P\243lz\274\247#5 jpjZp4\200\201\301\247\210\375i\256\230\250H\250\330Tl*&\025\021\024\302)\204Sv\321\266\227\024\345\342\246Y\rI\346R\211i\302Zp\2234\360\364\340\374Q\346\032n\354\321\232PsOZ\221IS\355S\251\317J\225EL\265 \024\340)\330\245\002\234\0058\nx\024\340(\305\024\204Q\212T8jRrh\017\212Fj\201\315@\324\306\254\206j\211\232\242c\232BqM\242\236)qM5\024\243\034\323\240\270\354j\322I\232\2247\255;9\247\nF\007\250\247\307q\3742\017\306\236\314\276\265\001\031\246\260\250\231j&Z\215\226\243+M+L\"\214Q\2121N\024\360)v\323\200\247\201R\001O\035(\002\214S\266\322\205\247\201R\001OPGJ\235:T\353R\250\247\201N\002\234\026\234\026\234\026\234\026\234\005.\3326SJ\321\212n(5\031\342\230Z\230y\250\3150\326\031j\215\2156\220\232J3J\r8\032By\250\2449\025\014C\347\253\253\362\324\352jA\355N\006\235\232f\321\232v\320p{\322\201\305#.FEF\313Q\262\324L\265\031Za\024\302\264\334R\342\214R\201O\002\234\005=P\323\302\323\200\247\250\247\205\030\240\256)@\247\001N\002\244\002\236\005H\243\0252\324\313R-<\nx\024\340\264\340\264\354T\2423\212i\\Q\212n)\n\323J\323H\250\315F\302\243\246\032i\256|\232a4\332BqH[\024\302\364\241\351\333\250\r\232k\232\205N\032\255+\344T\361\265J\017\024\360iA\247u\245\035)\312)\330\3151\226\243e\364\250\231j&Z\214\2550\212n)1K\212P)\300S\224T\352)\373h\333J\005<\n~(\331J\026\236\0058\n\221EH\005H\242\245Z\221jP*@)\340T\201iv\340\212pbh#\024\230\244\"\232E4\212k\n\211\205D\302\243\"\230i\206\271\322i\244\322TO%D_4\205\351\003\342\235\346P$\346\234[5\033t\342\235n\371m\254j\370\030\251\025\251\340\323\3058S\324S\205<R5V\226P\247\024\335\331\355HT\032\215\226\242+M\333M\333K\266\227\024\001N\025\"\234T\201\251\302\234\005H\253O\013N\333K\266\224\nx\025\"\212x\024\345\251\205H\242\246QR(\251Uj@\270\245\003\232VQ\214\212i\311\034\322Rb\223\024\204S\010\250\331j\026Z\211\205FEFk\234\244<T.\335\205Ws\3150\2750\275&\372\013R\253\363\315J\257RpED\312U\262\265b+\223\300z\270\2370\342\245\034S\325\252AO\034\323\305>\232\325\237u\301\247\301\312\214\324\333\007jk!\250\231)\205)6\322m\244\333F\3321N\247\212\221MH*E\251\005-8R\201N\003\322\234*AR(\251\005H\2652\324\350*eZ~\332\220\240\t\232\217\024\302)1F)\r!\024\322)\214*\027\025\013-D\302\242a\\\331\353Lv\300\252\254\325\0136j3M&\233\2327S\263R+T\310sO\333L+\203\232\225.\031:U\313k\201\'\r\326\257\252\251\035\205\007`\007\232\024\203\322\244\006\224\265C$\241G\275R\226O9\300\035\005Y\215p\005J)OJaZaZaZiZM\264m\244\305.)@\247\212x5\"\232\22058\032vi\302\236:S\2058T\253R\255J\242\245Z\235\rN\206\245\003=($\3644\303L4\224Q\214\323\274\2763Q\021Lj\211\205B\302\242aP\260\256d\232\2573UGja4\302i\204\323I\244\31585H\246\246\215\271\253jF)\255\3157fjH\243 \344U\261+\001\201Mb\344pi\251<\221q\326\245K\341\2347\024\367\277\215W\206\346\251\231\344\270l(\300\253pC\264s\326\254\201\212Z\0014\204\2323\352(\3004\322\264\233(\331I\345\321\262\215\224\270\245\002\226\236\016)\352i\342\245QR\001N\013\351O\002\236\0075\"\324\212je5*\232\235MH\255\212R\324\302i\231\346\226\212T\344\325\223\235\234UV\0305\031\025\023\n\205\205D\325\013\n\345\\\340U)\033&\240&\230M4\234\323M4\322P*E\251\220\324\353%.\374\232\225Njej\225\006j\\\niAPI\000jbY\202y\253\321@\2508\025>1K\364\240\nZ1\232]\264\340\202\227`\240\306)\273\005&\3126\322l\3154\255\030\305\030\315.)\352*U\025*\212\225V\244QO\013\232v\302)\3123O\003\024\365\251T\324\252\325 j\\\322SM(4\3602)\007\312r*_;#\232\205\216M0\323\030T\016*\026\025\013\n\343fz\250\315\232\214\232a4\224\206\232i)@\247\003OSR\203J\t\006\247F\251\224\214\325\204z\225H\247\022(\306i\350\2650\030\034R\321\214\322\205\247b\214R\201N\002\234\005.)\010\244\305&\3326\322\024\246\354>\224m=\305.\332r\255H\253R\250\251TT\252\265*\257\2558\014PW\373\264\003\353O\003\322\234)\340\323\324\323\203R\203A\244\006\244CO#5\031\244=i\244S\032\242a\232\205\205D\302\270)\0375\t4\302i\264R\023M4\224\023@4\3655\"\265J\246\244Z\231ML\206\246\006\236\274\324\300qO\035)\324\352v)@\247b\224\np\024\270\247\001K\212\000\245\332)\245Gj\002\203K\264\nLz\322\021\352(\013N\tO\013R*\324\252\265\"\373S\305;u\031\346\203\203\365\241\01685(\347\2458S\300\245\242\235\326\234\200g\2321\203\305;4\322i3Hi\206\243j\211\205B\302\274\355\215FM4\232m\031\244\315!4\334\321@\247\212z\232\231\rL\265(\036\225\"\261\251\326\246Z\220\032x\247\212p\024\361N\024\340)\300S\261@\024\354R\342\212Bh\244\2434\023FE\002\244\002\236\277J\220})\340\372\323\263F\352\\\322\346\224\032p9\353N\\\212\231H>\306\236\005?\024\252\007~izg\024\001A\024\224\206\222\220\232i\246\032\215\207\245D\302\274\321\2150\232i4\231\367\244\315!ji4\231\245\006\234)\342\236\265*\n\235je\251\224T\252*E\251\224T\200S\200\247\201O\002\236\005.)pi\324\240R\321Hi)3F\352JL\322\322\203N\r\212\221\\T\201\251wR\203N\315(4\271\245\315(jz\265J\0105*\277@\177:\224\032x\366\245\002\220\320y\024\322\010\246\322u\244\244\"\230E4\212k.k\313\t\246\223M&\232M!4\231\244\242\234\0058T\200T\212*e\0252\n\231EL\242\246QR\001R(\251\005<\nx\024\360)\340R\212ZZ3Fi3Fi\t\246\223I\2328\307\275\007\2123\212\001\247\003\232^\247\212\220\034S\303S\203S\201\247Q\232\\\321\232p8\247\253\324\253&jU\223\037J\225^\244\017\232RE&\352BsM4\235\250\372\323M\030\246\342\232Ey94\204\323i\264\332\r\024\240S\300\247\201R(\251\024T\310\265:-L\242\245QR\250\251\024T\252*@)\340S\300\247\001O\002\227\024b\203HM4\232L\322\023I\232L\321\272\214\323\272\320\t\034v\241\207qJ\275)\303\245<\021J\r<S\201\247R\216\264\264R\346\200i\301\252E\222\245W\251\003\323\304\236\264\273\2517Q\272\214\363\305\005\251\013Rn\244$SI\025\344\331\244\244&\232M%\024\240S\300\247\001R\001R(\251\225jeZ\225EL\242\245QR(\251TT\212*@*@)\340S\261K\214t\245\006\203M&\232i\244\322f\233\232\t\244\2434\231\247\003N\004\366\247\014\343\236iW\234\346\227\214P)\300\323\305<S\2058R\321E\024S\263N\r\212\221^\236\036\224=.\3727R\357\245\335I\272\232M!jaj\362\274\322\023M&\222\212P)\300S\300\247\201R(\251\024T\352*U\025*\212\225EJ\242\245QR\250\251\000\247\201O\002\236)sE\031\246\2264\026\3154\232L\323sIE!4\206\200sJ\r=i\331\305\024\2434\341N\247\003O\006\236\r8\032Z)GJR(\244\"\200is\212P\364\355\364\273\350\337J\036\227u\033\350\335A4\302k\313(\'\024\332)E-<\nx\024\360*E\0252\255L\242\245QR\250\251\024T\312*U\025\"\324\202\236\005>\2274\231\245\315&i3IHE4\212L\322f\215\302\223\"\223\255\030\305/\326\224\023N\025 \245\247R\212u8\032p4\340isN\024\242\224\n\\PE2\227&\214PM&qF\352\003R\357\240\275 zx|\322\023_\377\331"
+byte_png: "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\002\000\000\000\002\000\010\000\000\000\000\321\023\213&\000\000\003,IDATx^\355\335\335r\263 \020\000\320\214\357\377\314\231\357\247^\264:QQ(\260{\316]\2155\215\254\013,\306\276^\324\360\336n\000\000\000\000\242R\006\000\000\200r\306\321P`\331n\210@\026\270+d8\000\000\000\000\300\240\324\343\000\000fb\035\032\000\000\000 \253\307\353zJK\220\316\343\274\001\000\000\000\000@\037\n\274@\t9#\261\326\215\357F\003\000\000\000\"2\337\375\311\371\000\230C\353z0\000\320\231\311\331=\261\007I\261?\035\000\000\000\337\230\002\002\000\000@\036ow\010\000@J\326\002 )\343\377\334\264?\000\000\000\300\324\324\366\001\000\000 0\367u\000\000\000\000\000\000\000\300)\313\353y\371N\00506Y*=!\000\000\211x\252?\000\000\000\307L\034\271\302\322\002\000\000\000\000\000\000\000\000\000\000\000\304\347\373\003\000\000\000\000\000\323Q\332\005\000\200\346\026\343n\000\000\000\000\000\000\000\306gu\033\000\000\000\000\000\000\000\000\350\316\215L\000\000$b\370\233\331{\273\001\000\000\000\000\000\022\361\317|\000V\026\0169\262\215\217\355\317\3718\003\000\000\000\000\000\000\201Y\014\312\300MC\214\357N\224\272\037\016\000\000\000Z0\341\006\000\000\000\202Q\356\000\000\000\000\210F\305\007\000\000(\342\271\032\344 \322\001\000\000B3\355\003\370D\206\004\000\200\021\271\343\233j\004\023\3004\244l\000\000\332\263>\014\000\000\220K\257\325\207\306\357kz\013<\3208C\001\000\300\2573\306\005\000\000\340\216O\363\311\375r\354\317-\333\337\333\357?\276\345\257\355\266i\254\'\374\376\007(j\261\363\2679\337\003\310K\206\000\350\257h\360\307\320\264e \006I1]\276Hk\004@\215cp\317\345\206\006\000\000\000xB\021\002\200\036\364?\311\t\000\340\036\353\327\305$\\\000f\243\273\007\000\010\242h`W\2643\000\014M\257\266\223k\251B\000\000_r\345\276\007\244\315\272v\201\267\333P\231\006\344\221\377\001\324:J\031\334[\004\260\022\n\311\t\000\200tL\'!%\227>\000[\372\006 \275\2654\232\257B\252\007\000\000\000\000\000RQ\024\005HNG\000\337\344[ \377]\303\236\337\257?\314\327I\233\321\325\000\000\00000\005\201\030\224\037\000\370Hg\237\233\366\207C\023_\"\2139\000\'&\016o\036\221\034r8\270\302\017^b<\325\233\253\372\001\211L\2171\213\253\027\266\026\315\355j\234\000\323\223\356os\352\206\246#\003\000\200r\2469\311M\033\000\255g\200N\0140\245i\223\027Uh\177`X\022TX\0361GWM\002\260\3051y\350\270\0279~\025\200n\364\251\000\3008L\035\353i5\312ku\\xI\001\2759\377\364!\362\206\241\217ON\000\000\000P\235\t\037+\217\320\316N\000d\367/\002N\352\016\313\351\036\244 \n !\027>@R:\000\000\200F\024\3449d$\016\000\361U\350\357+\034\002\200N\344p\000\000\000\000\000\000\000\000\000\000\000\006\362\007Wv]\t\272\351Z\n\000\000\000\000IEND\256B`\202"
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index abbff58..c8a65a7 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1709,6 +1709,13 @@
            for more details.
         -->
         <flag name="shortService" value="0x800" />
+        <!-- The file management use case which manages files/directories, often involving file I/O
+            across the file system.
+            <p>Requires the app to hold the permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_FILE_MANAGEMENT} in order to use
+            this type.
+        -->
+        <flag name="fileManagement" value="0x1000" />
         <!-- Use cases that can't be categorized into any other foreground service types, but also
             can't use @link android.app.job.JobInfo.Builder} APIs.
             See {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE} for the
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index f2a16d3..d40adf5 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -118,6 +118,11 @@
     <bool name="config_using_subscription_manager_service">false</bool>
     <java-symbol type="bool" name="config_using_subscription_manager_service" />
 
+    <!-- Whether asynchronously update the subscription database or not. Async mode increases
+         the performance, but sync mode reduces the chance of database/cache out-of-sync. -->
+    <bool name="config_subscription_database_async_update">true</bool>
+    <java-symbol type="bool" name="config_subscription_database_async_update" />
+
     <!-- Boolean indicating whether the emergency numbers for a country, sourced from modem/config,
          should be ignored if that country is 'locked' (i.e. ignore_modem_config set to true) in
          Android Emergency DB. If this value is true, emergency numbers for a country, sourced from
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d0372ea..39012ae 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1201,6 +1201,11 @@
     <string name="permdesc_foregroundServiceSystemExempted">Allows the app to make use of foreground services with the type \"systemExempted\"</string>
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceFileManagement">run foreground service with the type \"fileManagement\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceFileManagement">Allows the app to make use of foreground services with the type \"fileManagement\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_foregroundServiceSpecialUse">run foreground service with the type \"specialUse\"</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_foregroundServiceSpecialUse">Allows the app to make use of foreground services with the type \"specialUse\"</string>
@@ -6402,4 +6407,22 @@
 
     <!-- Display content to tell the user the sim card name and number-->
     <string name="default_card_name">CARD <xliff:g id="cardNumber" example="1">%d</xliff:g></string>
+
+    <!-- Strings for CompanionDeviceManager -->
+    <!-- Title of Watch profile permission, which allows a companion app to manage watches. [CHAR LIMIT=NONE] -->
+    <string name="permlab_companionProfileWatch">Companion Watch profile permission to manage watches</string>
+    <!-- Description of Watch profile permission, which allows a companion app to manage watches. [CHAR LIMIT=NONE] -->
+    <string name="permdesc_companionProfileWatch">Allows a companion app to manage watches.</string>
+    <!-- Title of observing device presence permission, which allows a companion app to observe the presence of the associated devices. [CHAR LIMIT=NONE] -->
+    <string name="permlab_observeCompanionDevicePresence">Observe companion device presence</string>
+    <!-- Description of observing device presence permission, which allows a companion app to observe the presence of the associated devices. [CHAR LIMIT=NONE] -->
+    <string name="permdesc_observeCompanionDevicePresence">Allows a companion app to observe companion device presence when the devices are nearby or far-away.</string>
+    <!-- Title of delivering companion messages permission, which allows a companion app to deliver messages to other devices. [CHAR LIMIT=NONE] -->
+    <string name="permlab_deliverCompanionMessages">Deliver companion messages</string>
+    <!-- Description of delivering companion messages permission, which allows a companion app to deliver messages to other devices. [CHAR LIMIT=NONE] -->
+    <string name="permdesc_deliverCompanionMessages">Allows a companion app to deliver companion messages to other devices.</string>
+    <!-- Title of start foreground services from background permission [CHAR LIMIT=NONE] -->
+    <string name="permlab_startForegroundServicesFromBackground">Start foreground services from background</string>
+    <!-- Description of start foreground services from background permission [CHAR LIMIT=NONE] -->
+    <string name="permdesc_startForegroundServicesFromBackground">Allows a companion app to start foreground services from background.</string>
 </resources>
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
index 352c6a7..aa1853f 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
@@ -857,7 +857,10 @@
         ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
         String compositeName = namespace + "/" + key;
         Bundle result = resolver.call(
-                DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, compositeName, null);
+                Settings.Config.CONTENT_URI,
+                Settings.CALL_METHOD_DELETE_CONFIG,
+                compositeName,
+                null);
         assertThat(result).isNotNull();
         return compositeName.equals(result.getString(Settings.NameValueTable.VALUE));
     }
diff --git a/core/tests/coretests/src/android/provider/NameValueCacheTest.java b/core/tests/coretests/src/android/provider/NameValueCacheTest.java
index ee0b127..2e31bb5 100644
--- a/core/tests/coretests/src/android/provider/NameValueCacheTest.java
+++ b/core/tests/coretests/src/android/provider/NameValueCacheTest.java
@@ -76,7 +76,7 @@
         when(mMockContentProvider.getIContentProvider()).thenReturn(mMockIContentProvider);
         mMockContentResolver = new MockContentResolver(InstrumentationRegistry
                 .getInstrumentation().getContext());
-        mMockContentResolver.addProvider(DeviceConfig.CONTENT_URI.getAuthority(),
+        mMockContentResolver.addProvider(Settings.Config.CONTENT_URI.getAuthority(),
                 mMockContentProvider);
         mCacheGenerationStore = new MemoryIntArray(1);
         mStorage = new HashMap<>();
@@ -84,7 +84,7 @@
         // Stores keyValues for a given prefix and increments the generation. (Note that this
         // increments the generation no matter what, it doesn't pay attention to if anything
         // actually changed).
-        when(mMockIContentProvider.call(any(), eq(DeviceConfig.CONTENT_URI.getAuthority()),
+        when(mMockIContentProvider.call(any(), eq(Settings.Config.CONTENT_URI.getAuthority()),
                 eq(Settings.CALL_METHOD_SET_ALL_CONFIG),
                 any(), any(Bundle.class))).thenAnswer(invocationOnMock -> {
                     Bundle incomingBundle = invocationOnMock.getArgument(4);
@@ -104,7 +104,7 @@
         // Returns the keyValues corresponding to a namespace, or an empty map if the namespace
         // doesn't have anything stored for it. Returns the generation key if the caller asked
         // for one.
-        when(mMockIContentProvider.call(any(), eq(DeviceConfig.CONTENT_URI.getAuthority()),
+        when(mMockIContentProvider.call(any(), eq(Settings.Config.CONTENT_URI.getAuthority()),
                 eq(Settings.CALL_METHOD_LIST_CONFIG),
                 any(), any(Bundle.class))).thenAnswer(invocationOnMock -> {
                     Bundle incomingBundle = invocationOnMock.getArgument(4);
diff --git a/core/tests/coretests/src/android/provider/SettingsProviderTest.java b/core/tests/coretests/src/android/provider/SettingsProviderTest.java
index 4adbc91..1331779 100644
--- a/core/tests/coretests/src/android/provider/SettingsProviderTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsProviderTest.java
@@ -345,27 +345,33 @@
         try {
             // value is empty
             Bundle results =
-                    r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+                    r.call(Settings.Config.CONTENT_URI,
+                           Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertNull(results.get(Settings.NameValueTable.VALUE));
 
             // save value
-            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
+            results = r.call(Settings.Config.CONTENT_URI,
+                             Settings.CALL_METHOD_PUT_CONFIG, name, args);
             assertNull(results);
 
             // value is no longer empty
-            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+            results = r.call(Settings.Config.CONTENT_URI,
+                             Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertEquals(value, results.get(Settings.NameValueTable.VALUE));
 
             // save new value
             args.putString(Settings.NameValueTable.VALUE, newValue);
-            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
+            r.call(Settings.Config.CONTENT_URI,
+                    Settings.CALL_METHOD_PUT_CONFIG, name, args);
 
             // new value is returned
-            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+            results = r.call(Settings.Config.CONTENT_URI,
+                             Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertEquals(newValue, results.get(Settings.NameValueTable.VALUE));
         } finally {
             // clean up
-            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
+            r.call(Settings.Config.CONTENT_URI,
+                    Settings.CALL_METHOD_DELETE_CONFIG, name, null);
         }
     }
 
@@ -379,23 +385,25 @@
 
         try {
             // save value
-            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
+            r.call(Settings.Config.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
 
             // get value
             Bundle results =
-                    r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+                    r.call(Settings.Config.CONTENT_URI,
+                            Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertEquals(value, results.get(Settings.NameValueTable.VALUE));
 
             // delete value
-            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name,
+            results = r.call(Settings.Config.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name,
                     null);
 
             // value is empty now
-            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+            results = r.call(Settings.Config.CONTENT_URI,
+                            Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertNull(results.get(Settings.NameValueTable.VALUE));
         } finally {
             // clean up
-            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
+            r.call(Settings.Config.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
         }
     }
 
@@ -413,12 +421,12 @@
 
         try {
             // save both values
-            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
+            r.call(Settings.Config.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
             args.putString(Settings.NameValueTable.VALUE, newValue);
-            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, newName, args);
+            r.call(Settings.Config.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, newName, args);
 
             // list all values
-            Bundle result = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_LIST_CONFIG,
+            Bundle result = r.call(Settings.Config.CONTENT_URI, Settings.CALL_METHOD_LIST_CONFIG,
                     null, null);
             Map<String, String> keyValueMap =
                     (HashMap) result.getSerializable(Settings.NameValueTable.VALUE);
@@ -428,14 +436,15 @@
 
             // list values for prefix
             args.putString(Settings.CALL_METHOD_PREFIX_KEY, prefix);
-            result = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_LIST_CONFIG, null, args);
+            result = r.call(Settings.Config.CONTENT_URI,
+                            Settings.CALL_METHOD_LIST_CONFIG, null, args);
             keyValueMap = (HashMap) result.getSerializable(Settings.NameValueTable.VALUE);
             assertThat(keyValueMap, aMapWithSize(1));
             assertEquals(value, keyValueMap.get(name));
         } finally {
             // clean up
-            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
-            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, newName, null);
+            r.call(Settings.Config.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
+            r.call(Settings.Config.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, newName, null);
         }
     }
 }
diff --git a/core/tests/coretests/src/android/view/InsetsVisibilitiesTest.java b/core/tests/coretests/src/android/view/InsetsVisibilitiesTest.java
deleted file mode 100644
index 5664e0b..0000000
--- a/core/tests/coretests/src/android/view/InsetsVisibilitiesTest.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.view;
-
-import static android.view.InsetsState.FIRST_TYPE;
-import static android.view.InsetsState.LAST_TYPE;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
-
-import android.platform.test.annotations.Presubmit;
-import android.view.InsetsState.InternalInsetsType;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Tests for {@link InsetsVisibilities}.
- *
- * <p>Build/Install/Run:
- *  atest FrameworksCoreTests:InsetsVisibilities
- *
- * <p>This test class is a part of Window Manager Service tests and specified in
- * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}.
- */
-@Presubmit
-@RunWith(AndroidJUnit4.class)
-public class InsetsVisibilitiesTest {
-
-    @Test
-    public void testEquals() {
-        final InsetsVisibilities v1 = new InsetsVisibilities();
-        final InsetsVisibilities v2 = new InsetsVisibilities();
-        final InsetsVisibilities v3 = new InsetsVisibilities();
-        assertEquals(v1, v2);
-        assertEquals(v1, v3);
-
-        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
-            v1.setVisibility(type, false);
-            v2.setVisibility(type, false);
-        }
-        assertEquals(v1, v2);
-        assertNotEquals(v1, v3);
-
-        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
-            v1.setVisibility(type, true);
-            v2.setVisibility(type, true);
-        }
-        assertEquals(v1, v2);
-        assertNotEquals(v1, v3);
-    }
-
-    @Test
-    public void testSet() {
-        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
-            final InsetsVisibilities v1 = new InsetsVisibilities();
-            final InsetsVisibilities v2 = new InsetsVisibilities();
-
-            v1.setVisibility(type, true);
-            assertNotEquals(v1, v2);
-
-            v2.set(v1);
-            assertEquals(v1, v2);
-
-            v2.setVisibility(type, false);
-            assertNotEquals(v1, v2);
-
-            v1.set(v2);
-            assertEquals(v1, v2);
-        }
-    }
-
-    @Test
-    public void testCopyConstructor() {
-        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
-            final InsetsVisibilities v1 = new InsetsVisibilities();
-            v1.setVisibility(type, true);
-            final InsetsVisibilities v2 = new InsetsVisibilities(v1);
-            assertEquals(v1, v2);
-
-            v2.setVisibility(type, false);
-            assertNotEquals(v1, v2);
-        }
-    }
-
-    @Test
-    public void testGetterAndSetter() {
-        final InsetsVisibilities v1 = new InsetsVisibilities();
-        final InsetsVisibilities v2 = new InsetsVisibilities();
-
-        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
-            assertEquals(InsetsState.getDefaultVisibility(type), v1.getVisibility(type));
-        }
-
-        for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
-            v1.setVisibility(type, true);
-            assertTrue(v1.getVisibility(type));
-
-            v2.setVisibility(type, false);
-            assertFalse(v2.getVisibility(type));
-        }
-    }
-}
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index f370ebd..9d6b29e 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -17,6 +17,7 @@
 package android.window;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
@@ -60,8 +61,8 @@
     private OnBackAnimationCallback mCallback1;
     @Mock
     private OnBackAnimationCallback mCallback2;
-    @Mock
-    private BackEvent mBackEvent;
+    private final BackMotionEvent mBackEvent = new BackMotionEvent(
+            0, 0, 0, BackEvent.EDGE_LEFT, null);
 
     @Before
     public void setUp() throws Exception {
@@ -89,12 +90,12 @@
                 captor.capture());
         captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted(mBackEvent);
+        verify(mCallback1).onBackStarted(any(BackEvent.class));
         verifyZeroInteractions(mCallback2);
 
         captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted(mBackEvent);
+        verify(mCallback2).onBackStarted(any(BackEvent.class));
         verifyNoMoreInteractions(mCallback1);
     }
 
@@ -114,7 +115,7 @@
         assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY);
         captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted(mBackEvent);
+        verify(mCallback1).onBackStarted(any(BackEvent.class));
     }
 
     @Test
@@ -152,6 +153,6 @@
         verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture());
         captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted(mBackEvent);
+        verify(mCallback2).onBackStarted(any(BackEvent.class));
     }
 }
diff --git a/core/tests/fuzzers/FuzzService/FuzzBinder.java b/core/tests/fuzzers/FuzzService/FuzzBinder.java
index 52aafeb..7fd199a 100644
--- a/core/tests/fuzzers/FuzzService/FuzzBinder.java
+++ b/core/tests/fuzzers/FuzzService/FuzzBinder.java
@@ -34,12 +34,12 @@
         fuzzServiceInternal(binder, data);
     }
 
-    // This API creates random parcel object
-    public static void createRandomParcel(Parcel parcel, byte[] data) {
-        getRandomParcel(parcel, data);
+    // This API fills parcel object
+    public static void fillRandomParcel(Parcel parcel, byte[] data) {
+        fillParcelInternal(parcel, data);
     }
 
     private static native void fuzzServiceInternal(IBinder binder, byte[] data);
-    private static native void getRandomParcel(Parcel parcel, byte[] data);
+    private static native void fillParcelInternal(Parcel parcel, byte[] data);
     private static native int registerNatives();
 }
diff --git a/core/tests/fuzzers/FuzzService/random_parcel_jni.cpp b/core/tests/fuzzers/FuzzService/random_parcel_jni.cpp
index dbeae87..264aa5f 100644
--- a/core/tests/fuzzers/FuzzService/random_parcel_jni.cpp
+++ b/core/tests/fuzzers/FuzzService/random_parcel_jni.cpp
@@ -38,7 +38,7 @@
     return registerFrameworkNatives(env);
 }
 
-JNIEXPORT void JNICALL Java_randomparcel_FuzzBinder_getRandomParcel(JNIEnv *env, jobject thiz, jobject jparcel, jbyteArray fuzzData) {
+JNIEXPORT void JNICALL Java_randomparcel_FuzzBinder_fillParcelInternal(JNIEnv *env, jobject thiz, jobject jparcel, jbyteArray fuzzData) {
     size_t len = static_cast<size_t>(env->GetArrayLength(fuzzData));
     uint8_t data[len];
     env->GetByteArrayRegion(fuzzData, 0, len, reinterpret_cast<jbyte*>(data));
diff --git a/core/tests/fuzzers/FuzzService/random_parcel_jni.h b/core/tests/fuzzers/FuzzService/random_parcel_jni.h
index bc18b2f..c96354a 100644
--- a/core/tests/fuzzers/FuzzService/random_parcel_jni.h
+++ b/core/tests/fuzzers/FuzzService/random_parcel_jni.h
@@ -24,5 +24,5 @@
     // Function from AndroidRuntime
     jint registerFrameworkNatives(JNIEnv* env);
 
-    JNIEXPORT void JNICALL Java_randomparcel_FuzzBinder_getRandomParcel(JNIEnv *env, jobject thiz, jobject parcel, jbyteArray fuzzData);
+    JNIEXPORT void JNICALL Java_randomparcel_FuzzBinder_fillParcelInternal(JNIEnv *env, jobject thiz, jobject parcel, jbyteArray fuzzData);
 }
diff --git a/core/tests/fuzzers/ParcelFuzzer/Android.bp b/core/tests/fuzzers/ParcelFuzzer/Android.bp
new file mode 100644
index 0000000..b71a06e
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/Android.bp
@@ -0,0 +1,40 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_fuzz {
+    name: "java_binder_parcel_fuzzer",
+    srcs: [
+        "ParcelFuzzer.java",
+        "ReadUtils.java",
+        "FuzzUtils.java",
+        "FuzzOperation.java",
+        "ReadOperation.java",
+        ":framework-core-sources-for-fuzzers",
+    ],
+    static_libs: [
+        "jazzer",
+        "random_parcel_lib",
+        "binderReadParcelIface-java",
+    ],
+    jni_libs: [
+        "librandom_parcel_jni",
+        "libc++",
+        "libandroid_runtime",
+    ],
+    libs: [
+        "framework",
+        "unsupportedappusage",
+        "ext",
+        "framework-res",
+    ],
+    native_bridge_supported: true,
+    fuzz_config: {
+        cc: [
+            "smoreland@google.com",
+            "waghpawan@google.com",
+        ],
+        // Adds bugs to hotlist "AIDL fuzzers bugs" on buganizer
+        hotlists: ["4637097"],
+    },
+}
diff --git a/core/java/android/window/BackEvent.aidl b/core/tests/fuzzers/ParcelFuzzer/FuzzOperation.java
similarity index 78%
copy from core/java/android/window/BackEvent.aidl
copy to core/tests/fuzzers/ParcelFuzzer/FuzzOperation.java
index 821f1fa..033231d 100644
--- a/core/java/android/window/BackEvent.aidl
+++ b/core/tests/fuzzers/ParcelFuzzer/FuzzOperation.java
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package parcelfuzzer;
 
-package android.window;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
 
-/**
- * @hide
- */
-parcelable BackEvent;
+public interface FuzzOperation {
+    void doFuzz(FuzzedDataProvider data);
+}
diff --git a/core/tests/fuzzers/ParcelFuzzer/FuzzUtils.java b/core/tests/fuzzers/ParcelFuzzer/FuzzUtils.java
new file mode 100644
index 0000000..5e9e5ec
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/FuzzUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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 parcelfuzzer;
+
+import android.os.Parcel;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import randomparcel.FuzzBinder;
+
+public class FuzzUtils {
+    public static FuzzOperation[] FUZZ_OPERATIONS =
+            new FuzzOperation[] {
+                new FuzzOperation() {
+                    @java.lang.Override
+                    public void doFuzz(FuzzedDataProvider provider) {
+                        // Fuzz Append
+                        int start = provider.consumeInt();
+                        int len = provider.consumeInt();
+                        Parcel p1 = null;
+                        Parcel p2 = null;
+
+                        try {
+                            p1 = Parcel.obtain();
+                            p2 = Parcel.obtain();
+
+                            byte[] data =
+                                    provider.consumeBytes(
+                                            provider.consumeInt(0, provider.remainingBytes()));
+                            FuzzBinder.fillRandomParcel(p1, data);
+                            FuzzBinder.fillRandomParcel(p2, provider.consumeRemainingAsBytes());
+
+                            p1.appendFrom(p2, start, len);
+
+                        } catch (Exception e) {
+                            // Rethrow exception as runtime exceptions are catched
+                            // at highest level.
+                            throw e;
+                        } finally {
+                            p1.recycle();
+                            p2.recycle();
+                        }
+                    }
+                },
+                new FuzzOperation() {
+                    @java.lang.Override
+                    public void doFuzz(FuzzedDataProvider provider) {
+                        // Fuzz Read
+                        // Use maximum bytes to generate read instructions and remaining for parcel
+                        // creation
+                        int maxParcelBytes = provider.remainingBytes() / 3;
+                        byte[] data = provider.consumeBytes(maxParcelBytes);
+                        Parcel randomParcel = null;
+
+                        try {
+                            randomParcel = Parcel.obtain();
+                            FuzzBinder.fillRandomParcel(randomParcel, data);
+
+                            while (provider.remainingBytes() > 0) {
+                                provider.pickValue(ReadUtils.READ_OPERATIONS)
+                                        .readParcel(randomParcel, provider);
+                            }
+
+                        } catch (Exception e) {
+                            // Rethrow exception as runtime exceptions are catched
+                            // at highest level.
+                            throw e;
+                        } finally {
+                            randomParcel.recycle();
+                        }
+                    }
+                },
+            };
+}
diff --git a/core/tests/fuzzers/ParcelFuzzer/ParcelFuzzer.java b/core/tests/fuzzers/ParcelFuzzer/ParcelFuzzer.java
new file mode 100644
index 0000000..688c812
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/ParcelFuzzer.java
@@ -0,0 +1,42 @@
+/*
+ * 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 parcelfuzzer;
+
+import android.util.Log;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import randomparcel.FuzzBinder;
+
+public class ParcelFuzzer {
+
+    static {
+        // Initialize JNI dependencies
+        FuzzBinder.init();
+    }
+
+    public static void fuzzerTestOneInput(FuzzedDataProvider provider) {
+        // Default behavior for Java APIs is to throw RuntimeException.
+        // We need to fuzz to detect other problems which are not handled explicitly.
+        // TODO(b/150808347): Change known API exceptions to subclass of
+        // RuntimeExceptions and catch those only.
+        try {
+            provider.pickValue(FuzzUtils.FUZZ_OPERATIONS).doFuzz(provider);
+        } catch (RuntimeException e) {
+            Log.e("ParcelFuzzer", "Exception occurred while fuzzing ", e);
+        }
+    }
+}
diff --git a/core/java/android/window/BackEvent.aidl b/core/tests/fuzzers/ParcelFuzzer/ReadOperation.java
similarity index 74%
copy from core/java/android/window/BackEvent.aidl
copy to core/tests/fuzzers/ParcelFuzzer/ReadOperation.java
index 821f1fa..5c227e3 100644
--- a/core/java/android/window/BackEvent.aidl
+++ b/core/tests/fuzzers/ParcelFuzzer/ReadOperation.java
@@ -13,10 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package parcelfuzzer;
 
-package android.window;
+import android.os.Parcel;
 
-/**
- * @hide
- */
-parcelable BackEvent;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+public interface ReadOperation {
+    void readParcel(Parcel parcel, FuzzedDataProvider provider);
+}
diff --git a/core/tests/fuzzers/ParcelFuzzer/ReadUtils.java b/core/tests/fuzzers/ParcelFuzzer/ReadUtils.java
new file mode 100644
index 0000000..0eff5f2
--- /dev/null
+++ b/core/tests/fuzzers/ParcelFuzzer/ReadUtils.java
@@ -0,0 +1,435 @@
+/*
+ * 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 parcelfuzzer;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import parcelables.EmptyParcelable;
+import parcelables.GenericDataParcelable;
+import parcelables.SingleDataParcelable;
+
+public class ReadUtils {
+    public static int MAX_LEN = 1000000;
+    public static int MIN_LEN = 0;
+
+    private static class SomeParcelable implements Parcelable {
+        private final int mValue;
+
+        private SomeParcelable(Parcel in) {
+            this.mValue = in.readInt();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(mValue);
+        }
+
+        public static Parcelable.Creator<SomeParcelable> CREATOR =
+                new Parcelable.Creator<SomeParcelable>() {
+
+                    @Override
+                    public SomeParcelable createFromParcel(Parcel source) {
+                        return new SomeParcelable(source);
+                    }
+
+                    @Override
+                    public SomeParcelable[] newArray(int size) {
+                        return new SomeParcelable[size];
+                    }
+                };
+    }
+
+    private static class TestClassLoader extends ClassLoader {
+        TestClassLoader() {
+            super();
+        }
+    }
+
+    private static class TestInterface implements IInterface {
+        public Binder binder;
+        private static final String DESCRIPTOR = "TestInterface";
+
+        TestInterface() {
+            binder = new Binder();
+            binder.attachInterface(this, DESCRIPTOR);
+        }
+
+        public IBinder asBinder() {
+            return binder;
+        }
+
+        public static TestInterface asInterface(IBinder binder) {
+            if (binder != null) {
+                IInterface iface = binder.queryLocalInterface(DESCRIPTOR);
+                if (iface != null && iface instanceof TestInterface) {
+                    return (TestInterface) iface;
+                }
+            }
+            return null;
+        }
+    }
+
+    public static ReadOperation[] READ_OPERATIONS =
+            new ReadOperation[] {
+                    (parcel, provider) -> {
+                        parcel.setDataPosition(provider.consumeInt());
+                    },
+                    (parcel, provider) -> {
+                        parcel.setDataCapacity(provider.consumeInt());
+                    },
+                    (parcel, provider) -> {
+                        parcel.setDataSize(provider.consumeInt());
+                    },
+                    (parcel, provider) -> {
+                        parcel.dataSize();
+                    },
+                    (parcel, provider) -> {
+                        parcel.dataPosition();
+                    },
+                    (parcel, provider) -> {
+                        parcel.dataCapacity();
+                    },
+
+                    // read basic types
+                    (parcel, provider) -> {
+                        parcel.readByte();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readBoolean();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readInt();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readLong();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readFloat();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readDouble();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readString();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readString8();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readString16();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readBlob();
+                    },
+                    (parcel, provider) -> {
+                        parcel.readStrongBinder();
+                    },
+
+                    // read arrays of random length
+                    (parcel, provider) -> {
+                        byte[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new byte[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new byte[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readByteArray(array);
+                    },
+                    (parcel, provider) -> {
+                        char[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new char[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new char[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readCharArray(array);
+                    },
+                    (parcel, provider) -> {
+                        int[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new int[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new int[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readIntArray(array);
+                    },
+                    (parcel, provider) -> {
+                        double[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new double[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new double[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readDoubleArray(array);
+                    },
+                    (parcel, provider) -> {
+                        float[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new float[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new float[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readFloatArray(array);
+                    },
+                    (parcel, provider) -> {
+                        boolean[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new boolean[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new boolean[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readBooleanArray(array);
+                    },
+                    (parcel, provider) -> {
+                        long[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new long[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new long[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readLongArray(array);
+                    },
+                    (parcel, provider) -> {
+                        IBinder[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new IBinder[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new IBinder[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readBinderArray(array);
+                    },
+                    (parcel, provider) -> {
+                        ArrayList<IBinder> arrayList = new ArrayList<IBinder>();
+                        parcel.readBinderList(arrayList);
+                    },
+
+                    // unmarshall from random parcel data and random bytes
+                    (parcel, provider) -> {
+                        byte[] data = parcel.marshall();
+                        Parcel p = Parcel.obtain();
+                        p.unmarshall(data, provider.consumeInt(), provider.consumeInt());
+                        p.recycle();
+                    },
+                    (parcel, provider) -> {
+                        byte[] data = provider.consumeRemainingAsBytes();
+                        Parcel p = Parcel.obtain();
+                        p.unmarshall(data, provider.consumeInt(), provider.consumeInt());
+                        p.recycle();
+                    },
+                    (parcel, provider) -> {
+                        parcel.hasFileDescriptors(provider.consumeInt(), provider.consumeInt());
+                    },
+
+                    // read AIDL generated parcelables
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelable(loader, SingleDataParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelableArray(loader, SingleDataParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        SingleDataParcelable[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new SingleDataParcelable[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new SingleDataParcelable[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readTypedArray(array, SingleDataParcelable.CREATOR);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelable(loader, EmptyParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelableArray(loader, EmptyParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        EmptyParcelable[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new EmptyParcelable[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new EmptyParcelable[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readTypedArray(array, EmptyParcelable.CREATOR);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelable(loader, GenericDataParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelableArray(loader, GenericDataParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        GenericDataParcelable[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new GenericDataParcelable[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            int len = provider.consumeInt(MIN_LEN, MAX_LEN);
+                            array = new GenericDataParcelable[len];
+                        }
+                        parcel.readTypedArray(array, GenericDataParcelable.CREATOR);
+                    },
+
+                    // read parcelables
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelable(loader, SomeParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelableArray(loader, SomeParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        SomeParcelable[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new SomeParcelable[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new SomeParcelable[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readTypedArray(array, SomeParcelable.CREATOR);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelableArray(loader);
+                    },
+                    (parcel, provider) -> {
+                        parcel.hasFileDescriptors(provider.consumeInt(), provider.consumeInt());
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readParcelableArray(loader);
+                    },
+
+                    // read lists
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readArrayList(loader);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readArrayList(loader, Object.class);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readArrayList(loader, SomeParcelable.class);
+                    },
+
+                    // read sparse arrays
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readSparseArray(loader);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readSparseArray(loader, Object.class);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readSparseArray(loader, SomeParcelable.class);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readSerializable(loader, Object.class);
+                    },
+
+                    // read interface
+                    (parcel, provider) -> {
+                        TestInterface[] array;
+                        if (provider.consumeBoolean()) {
+                            int pos = parcel.dataPosition();
+                            array = new TestInterface[Math.min(MAX_LEN, parcel.readInt())];
+                            parcel.setDataPosition(pos);
+                        } else {
+                            array = new TestInterface[provider.consumeInt(MIN_LEN, MAX_LEN)];
+                        }
+                        parcel.readInterfaceArray(array, TestInterface::asInterface);
+                    },
+                    (parcel, provider) -> {
+                        int w = provider.consumeInt(MIN_LEN, MAX_LEN);
+                        int h = provider.consumeInt(MIN_LEN, MAX_LEN);
+                        TestInterface[][] array = new TestInterface[w][h];
+                        parcel.readFixedArray(array, TestInterface::asInterface);
+                    },
+                    (parcel, provider) -> {
+                        ArrayList<TestInterface> array = new ArrayList<TestInterface>();
+                        parcel.readInterfaceList(array, TestInterface::asInterface);
+                    },
+
+                    // read bundle
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readBundle(loader);
+                    },
+                    (parcel, provider) -> {
+                        parcel.readBundle();
+                    },
+
+                    // read HashMap
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readHashMap(loader);
+                    },
+                    (parcel, provider) -> {
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readHashMap(loader, String.class, String.class);
+                    },
+                    (parcel, provider) -> {
+                        HashMap<String, String> hashMap = new HashMap<>();
+                        TestClassLoader loader = new TestClassLoader();
+                        parcel.readMap(hashMap, loader, String.class, String.class);
+                    },
+            };
+}
diff --git a/core/tests/fuzzers/java_service_fuzzer/Android.bp b/core/tests/fuzzers/java_service_fuzzer/Android.bp
index 625de14..6acb198 100644
--- a/core/tests/fuzzers/java_service_fuzzer/Android.bp
+++ b/core/tests/fuzzers/java_service_fuzzer/Android.bp
@@ -37,4 +37,12 @@
         "framework-res",
     ],
     native_bridge_supported: true,
+    fuzz_config: {
+        cc: [
+            "smoreland@google.com",
+            "waghpawan@google.com",
+        ],
+        // Adds bugs to hotlist "AIDL fuzzers bugs" on buganizer
+        hotlists: ["4637097"],
+    },
 }
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 4cc06e3..3346740 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -4171,6 +4171,12 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
+    "1945495497": {
+      "message": "Focused window didn't have a valid surface drawn.",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_BACK_PREVIEW",
+      "at": "com\/android\/server\/wm\/BackNavigationController.java"
+    },
     "1947239194": {
       "message": "Deferring rotation, still finishing previous rotation",
       "level": "VERBOSE",
diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java
index c93b733..68f2927 100644
--- a/graphics/java/android/graphics/ImageFormat.java
+++ b/graphics/java/android/graphics/ImageFormat.java
@@ -26,7 +26,7 @@
      @Retention(RetentionPolicy.SOURCE)
      @IntDef(value = {
              UNKNOWN,
-             /**
+             /*
               * Since some APIs accept either ImageFormat or PixelFormat (and the two
               * enums do not overlap since they're both partial versions of the
               * internal format enum), add PixelFormat values here so linting
diff --git a/keystore/java/android/security/KeyStoreException.java b/keystore/java/android/security/KeyStoreException.java
index 1a81dda..6536e43 100644
--- a/keystore/java/android/security/KeyStoreException.java
+++ b/keystore/java/android/security/KeyStoreException.java
@@ -138,6 +138,16 @@
      * provisioning server refuses key issuance, this is a permanent error.</p>
      */
     public static final int ERROR_ATTESTATION_KEYS_UNAVAILABLE = 16;
+    /**
+     * This device requires a software upgrade to use the key provisioning server. The software
+     * is outdated and this error is returned only on devices that rely solely on
+     * remotely-provisioned keys (see <a href=
+     * "https://android-developers.googleblog.com/2022/03/upgrading-android-attestation-remote.html"
+     * >Remote Key Provisioning</a>).
+     *
+     * @hide
+     */
+    public static final int ERROR_DEVICE_REQUIRES_UPGRADE_FOR_ATTESTATION = 17;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -157,7 +167,8 @@
             ERROR_INCORRECT_USAGE,
             ERROR_KEY_NOT_TEMPORALLY_VALID,
             ERROR_KEY_OPERATION_EXPIRED,
-            ERROR_ATTESTATION_KEYS_UNAVAILABLE
+            ERROR_ATTESTATION_KEYS_UNAVAILABLE,
+            ERROR_DEVICE_REQUIRES_UPGRADE_FOR_ATTESTATION,
     })
     public @interface PublicErrorCode {
     }
@@ -184,6 +195,16 @@
      * This value is returned when {@link #isTransientFailure()} is {@code true}.
      */
     public static final int RETRY_WHEN_CONNECTIVITY_AVAILABLE = 3;
+    /**
+     * Re-try the operation that led to this error when the device has a software update
+     * downloaded and on the next reboot. The Remote provisioning server recognizes
+     * the device, but refuses issuance of attestation keys because it contains a software
+     * version that could potentially be vulnerable and needs an update. Re-trying after the
+     * device has upgraded and rebooted may alleviate the problem.
+     *
+     * <p>This value is returned when {@link #isTransientFailure()} is {@code true}.
+     */
+    public static final int RETRY_AFTER_NEXT_REBOOT = 4;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -191,6 +212,7 @@
             RETRY_NEVER,
             RETRY_WITH_EXPONENTIAL_BACKOFF,
             RETRY_WHEN_CONNECTIVITY_AVAILABLE,
+            RETRY_AFTER_NEXT_REBOOT,
     })
     public @interface RetryPolicy {
     }
@@ -217,6 +239,13 @@
      * when the device has connectivity again.
      * @hide */
     public static final int RKP_FETCHING_PENDING_CONNECTIVITY = 3;
+    /**
+     * The RKP server recognizes the device, but the device may be running vulnerable software,
+     * and thus refusing issuance of RKP keys to it.
+     *
+     * @hide
+     */
+    public static final int RKP_FETCHING_PENDING_SOFTWARE_REBOOT = 4;
 
     // Constants for encoding information about the error encountered:
     // Whether the error relates to the system state/implementation as a whole, or a specific key.
@@ -236,7 +265,7 @@
     private static int initializeRkpStatusForRegularErrors(int errorCode) {
         // Check if the system code mistakenly called a constructor of KeyStoreException with
         // the OUT_OF_KEYS error code but without RKP status.
-        if (errorCode == ResponseCode.OUT_OF_KEYS) {
+        if (isRkpRelatedError(errorCode)) {
             Log.e(TAG, "RKP error code without RKP status");
             // Set RKP status to RKP_SERVER_REFUSED_ISSUANCE so that the caller never retries.
             return RKP_SERVER_REFUSED_ISSUANCE;
@@ -272,7 +301,7 @@
         super(message);
         mErrorCode = errorCode;
         mRkpStatus = rkpStatus;
-        if (mErrorCode != ResponseCode.OUT_OF_KEYS) {
+        if (!isRkpRelatedError(mErrorCode)) {
             Log.e(TAG, "Providing RKP status for error code " + errorCode + " has no effect.");
         }
     }
@@ -309,10 +338,11 @@
     public boolean isTransientFailure() {
         PublicErrorInformation failureInfo = getErrorInformation(mErrorCode);
         // Special-case handling for RKP failures:
-        if (mRkpStatus != RKP_SUCCESS && mErrorCode == ResponseCode.OUT_OF_KEYS) {
+        if (mRkpStatus != RKP_SUCCESS && isRkpRelatedError(mErrorCode)) {
             switch (mRkpStatus) {
                 case RKP_TEMPORARILY_UNAVAILABLE:
                 case RKP_FETCHING_PENDING_CONNECTIVITY:
+                case RKP_FETCHING_PENDING_SOFTWARE_REBOOT:
                     return true;
                 case RKP_SERVER_REFUSED_ISSUANCE:
                 default:
@@ -346,6 +376,11 @@
         return (failureInfo.indicators & IS_SYSTEM_ERROR) != 0;
     }
 
+    private static boolean isRkpRelatedError(int errorCode) {
+        return errorCode == ResponseCode.OUT_OF_KEYS
+                  || errorCode == ResponseCode.OUT_OF_KEYS_REQUIRES_UPGRADE;
+    }
+
     /**
      * Returns the re-try policy for transient failures. Valid only if
      * {@link #isTransientFailure()} returns {@code True}.
@@ -362,6 +397,8 @@
                     return RETRY_WHEN_CONNECTIVITY_AVAILABLE;
                 case RKP_SERVER_REFUSED_ISSUANCE:
                     return RETRY_NEVER;
+                case RKP_FETCHING_PENDING_SOFTWARE_REBOOT:
+                    return RETRY_AFTER_NEXT_REBOOT;
                 default:
                     return (failureInfo.indicators & IS_TRANSIENT_ERROR) != 0
                             ? RETRY_WITH_EXPONENTIAL_BACKOFF : RETRY_NEVER;
@@ -620,5 +657,8 @@
                 new PublicErrorInformation(0, ERROR_KEY_DOES_NOT_EXIST));
         sErrorCodeToFailureInfo.put(ResponseCode.OUT_OF_KEYS,
                 new PublicErrorInformation(IS_SYSTEM_ERROR, ERROR_ATTESTATION_KEYS_UNAVAILABLE));
+        sErrorCodeToFailureInfo.put(ResponseCode.OUT_OF_KEYS_REQUIRES_UPGRADE,
+                new PublicErrorInformation(IS_SYSTEM_ERROR | IS_TRANSIENT_ERROR,
+                        ERROR_DEVICE_REQUIRES_UPGRADE_FOR_ATTESTATION));
     }
 }
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index acc0005..2830d7eff 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -52,6 +52,7 @@
 import android.system.keystore2.KeyMetadata;
 import android.system.keystore2.ResponseCode;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 
@@ -647,6 +648,7 @@
         // {@link android.security.KeyStoreException#RKP_TEMPORARILY_UNAVAILABLE},
         // {@link android.security.KeyStoreException#RKP_SERVER_REFUSED_ISSUANCE},
         // {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_CONNECTIVITY}
+        // {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_SOFTWARE_REBOOT}
         public final int rkpStatus;
         @Nullable
         public final KeyPair keyPair;
@@ -856,6 +858,13 @@
                                 KeymasterDefs.KM_TAG_ATTESTATION_ID_IMEI,
                                 imei.getBytes(StandardCharsets.UTF_8)
                         ));
+                        final String secondImei = telephonyService.getImei(1);
+                        if (!TextUtils.isEmpty(secondImei)) {
+                            params.add(KeyStore2ParameterUtils.makeBytes(
+                                    KeymasterDefs.KM_TAG_ATTESTATION_ID_SECOND_IMEI,
+                                    secondImei.getBytes(StandardCharsets.UTF_8)
+                            ));
+                        }
                         break;
                     }
                     case AttestationUtils.ID_TYPE_MEID: {
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
index e6ae282..2994593 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
@@ -1,19 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
         android:width="48dp"
         android:height="48dp"
@@ -25,13 +25,12 @@
         android:fillAlpha="0.8"
         android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/>
     <group
-        android:translateX="12"
-        android:translateY="12">
+        android:scaleX="0.8"
+        android:scaleY="0.8"
+        android:translateX="10"
+        android:translateY="10">
         <path
-            android:fillColor="@color/compat_controls_text"
-            android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/>
-        <path
-            android:fillColor="@color/compat_controls_text"
-            android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/>
+            android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z"
+            android:fillColor="@color/compat_controls_text"/>
     </group>
 </vector>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 0133f6b..57a0fd5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -47,6 +47,7 @@
 import android.view.RemoteAnimationTarget;
 import android.window.BackAnimationAdapter;
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 import android.window.BackNavigationInfo;
 import android.window.IBackAnimationFinishedCallback;
 import android.window.IBackAnimationRunner;
@@ -385,7 +386,7 @@
             return;
         }
 
-        final BackEvent backEvent = mTouchTracker.createProgressEvent();
+        final BackMotionEvent backEvent = mTouchTracker.createProgressEvent();
         dispatchOnBackProgressed(mActiveCallback, backEvent);
     }
 
@@ -415,7 +416,7 @@
     }
 
     private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
-            BackEvent backEvent) {
+            BackMotionEvent backEvent) {
         if (callback == null) {
             return;
         }
@@ -453,7 +454,7 @@
     }
 
     private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
-            BackEvent backEvent) {
+            BackMotionEvent backEvent) {
         if (callback == null) {
             return;
         }
@@ -466,6 +467,11 @@
         }
     }
 
+    private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) {
+        // TODO(b/258698745): Only dispatch to animation callbacks.
+        return mEnableAnimations.get();
+    }
+
     /**
      * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
      */
@@ -640,7 +646,7 @@
                     if (!mBackGestureStarted) {
                         // if the down -> up gesture happened before animation start, we have to
                         // trigger the uninterruptible transition to finish the back animation.
-                        final BackEvent backFinish = mTouchTracker.createProgressEvent();
+                        final BackMotionEvent backFinish = mTouchTracker.createProgressEvent();
                         dispatchOnBackProgressed(mActiveCallback, backFinish);
                         startPostCommitAnimation();
                     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
index 9f6bc5d..e36e16c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityAnimation.java
@@ -39,6 +39,7 @@
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 import android.window.BackProgressAnimator;
 import android.window.IOnBackInvokedCallback;
 
@@ -315,13 +316,13 @@
 
     private final class Callback extends IOnBackInvokedCallback.Default {
         @Override
-        public void onBackStarted(BackEvent backEvent) {
+        public void onBackStarted(BackMotionEvent backEvent) {
             mProgressAnimator.onBackStarted(backEvent,
                     CrossActivityAnimation.this::onGestureProgress);
         }
 
         @Override
-        public void onBackProgressed(@NonNull BackEvent backEvent) {
+        public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
             mProgressAnimator.onBackProgressed(backEvent);
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index a9a7b77..676e259 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -39,6 +39,7 @@
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 import android.window.BackProgressAnimator;
 import android.window.IOnBackInvokedCallback;
 
@@ -316,13 +317,13 @@
 
     private final class Callback extends IOnBackInvokedCallback.Default  {
         @Override
-        public void onBackStarted(BackEvent backEvent) {
+        public void onBackStarted(BackMotionEvent backEvent) {
             mProgressAnimator.onBackStarted(backEvent,
                     CrossTaskBackAnimation.this::onGestureProgress);
         }
 
         @Override
-        public void onBackProgressed(@NonNull BackEvent backEvent) {
+        public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
             mProgressAnimator.onBackProgressed(backEvent);
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
index ccfac65..695ef4e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -19,6 +19,7 @@
 import android.os.SystemProperties;
 import android.view.RemoteAnimationTarget;
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 
 /**
  * Helper class to record the touch location for gesture and generate back events.
@@ -82,11 +83,11 @@
         mSwipeEdge = BackEvent.EDGE_LEFT;
     }
 
-    BackEvent createStartEvent(RemoteAnimationTarget target) {
-        return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
+    BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
+        return new BackMotionEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
     }
 
-    BackEvent createProgressEvent() {
+    BackMotionEvent createProgressEvent() {
         float progressThreshold = PROGRESS_THRESHOLD >= 0
                 ? PROGRESS_THRESHOLD : mProgressThreshold;
         progressThreshold = progressThreshold == 0 ? 1 : progressThreshold;
@@ -109,8 +110,8 @@
         return createProgressEvent(progress);
     }
 
-    BackEvent createProgressEvent(float progress) {
-        return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
+    BackMotionEvent createProgressEvent(float progress) {
+        return new BackMotionEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
     }
 
     public void setProgressThreshold(float progressThreshold) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index 6e116b9..c836b95 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -51,8 +51,6 @@
 import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.SurfaceUtils;
 
-import java.util.function.Consumer;
-
 /**
  * Handles split decor like showing resizing hint for a specific split.
  */
@@ -72,17 +70,18 @@
     private SurfaceControl mIconLeash;
     private SurfaceControl mBackgroundLeash;
     private SurfaceControl mGapBackgroundLeash;
+    private SurfaceControl mScreenshot;
 
     private boolean mShown;
     private boolean mIsResizing;
     private final Rect mBounds = new Rect();
-    private final Rect mResizingBounds = new Rect();
     private final Rect mTempRect = new Rect();
     private ValueAnimator mFadeAnimator;
 
     private int mIconSize;
     private int mOffsetX;
     private int mOffsetY;
+    private int mRunningAnimationCount = 0;
 
     public SplitDecorManager(Configuration configuration, IconProvider iconProvider,
             SurfaceSession surfaceSession) {
@@ -173,7 +172,6 @@
             mIsResizing = true;
             mBounds.set(newBounds);
         }
-        mResizingBounds.set(newBounds);
         mOffsetX = offsetX;
         mOffsetY = offsetY;
 
@@ -227,33 +225,41 @@
                 t.setVisibility(mBackgroundLeash, show);
                 t.setVisibility(mIconLeash, show);
             } else {
-                startFadeAnimation(show, null /* finishedConsumer */);
+                startFadeAnimation(show, false, null);
             }
             mShown = show;
         }
     }
 
     /** Stops showing resizing hint. */
-    public void onResized(SurfaceControl.Transaction t) {
-        if (!mShown && mIsResizing) {
-            mTempRect.set(mResizingBounds);
-            mTempRect.offsetTo(-mOffsetX, -mOffsetY);
-            final SurfaceControl screenshot = ScreenshotUtils.takeScreenshot(t,
-                    mHostLeash, mTempRect, Integer.MAX_VALUE - 1);
+    public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) {
+        if (mScreenshot != null) {
+            t.setPosition(mScreenshot, mOffsetX, mOffsetY);
 
             final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
             final ValueAnimator va = ValueAnimator.ofFloat(1, 0);
             va.addUpdateListener(valueAnimator -> {
                 final float progress = (float) valueAnimator.getAnimatedValue();
-                animT.setAlpha(screenshot, progress);
+                animT.setAlpha(mScreenshot, progress);
                 animT.apply();
             });
             va.addListener(new AnimatorListenerAdapter() {
                 @Override
+                public void onAnimationStart(Animator animation) {
+                    mRunningAnimationCount++;
+                }
+
+                @Override
                 public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
-                    animT.remove(screenshot);
+                    mRunningAnimationCount--;
+                    animT.remove(mScreenshot);
                     animT.apply();
                     animT.close();
+                    mScreenshot = null;
+
+                    if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+                        animFinishedCallback.run();
+                    }
                 }
             });
             va.start();
@@ -285,10 +291,34 @@
             mFadeAnimator.cancel();
         }
         if (mShown) {
-            fadeOutDecor(null /* finishedCallback */);
+            fadeOutDecor(animFinishedCallback);
         } else {
             // Decor surface is hidden so release it directly.
             releaseDecor(t);
+            if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+                animFinishedCallback.run();
+            }
+        }
+    }
+
+    /** Screenshot host leash and attach on it if meet some conditions */
+    public void screenshotIfNeeded(SurfaceControl.Transaction t) {
+        if (!mShown && mIsResizing) {
+            mTempRect.set(mBounds);
+            mTempRect.offsetTo(0, 0);
+            mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect,
+                    Integer.MAX_VALUE - 1);
+        }
+    }
+
+    /** Set screenshot and attach on host leash it if meet some conditions */
+    public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) {
+        if (screenshot == null || !screenshot.isValid()) return;
+
+        if (!mShown && mIsResizing) {
+            mScreenshot = screenshot;
+            t.reparent(screenshot, mHostLeash);
+            t.setLayer(screenshot, Integer.MAX_VALUE - 1);
         }
     }
 
@@ -296,18 +326,15 @@
      * directly. */
     public void fadeOutDecor(Runnable finishedCallback) {
         if (mShown) {
-            startFadeAnimation(false /* show */, transaction -> {
-                releaseDecor(transaction);
-                if (finishedCallback != null) finishedCallback.run();
-            });
+            startFadeAnimation(false /* show */, true, finishedCallback);
             mShown = false;
         } else {
             if (finishedCallback != null) finishedCallback.run();
         }
     }
 
-    private void startFadeAnimation(boolean show,
-            Consumer<SurfaceControl.Transaction> finishedConsumer) {
+    private void startFadeAnimation(boolean show, boolean releaseSurface,
+            Runnable finishedCallback) {
         final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
         mFadeAnimator = ValueAnimator.ofFloat(0f, 1f);
         mFadeAnimator.setDuration(FADE_DURATION);
@@ -324,6 +351,7 @@
         mFadeAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(@NonNull Animator animation) {
+                mRunningAnimationCount++;
                 if (show) {
                     animT.show(mBackgroundLeash).show(mIconLeash);
                 }
@@ -335,6 +363,7 @@
 
             @Override
             public void onAnimationEnd(@NonNull Animator animation) {
+                mRunningAnimationCount--;
                 if (!show) {
                     if (mBackgroundLeash != null) {
                         animT.hide(mBackgroundLeash);
@@ -343,11 +372,15 @@
                         animT.hide(mIconLeash);
                     }
                 }
-                if (finishedConsumer != null) {
-                    finishedConsumer.accept(animT);
+                if (releaseSurface) {
+                    releaseDecor(animT);
                 }
                 animT.apply();
                 animT.close();
+
+                if (mRunningAnimationCount == 0 && finishedCallback != null) {
+                    finishedCallback.run();
+                }
             }
         });
         mFadeAnimator.start();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 4ea8a5d..dc1634a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -697,10 +697,13 @@
 
     @WMSingleton
     @Provides
-    static Optional<DesktopModeController> providesDesktopModeController(
-            @DynamicOverride Optional<DesktopModeController> desktopModeController) {
+    static Optional<DesktopModeController> provideDesktopModeController(
+            @DynamicOverride Optional<Lazy<DesktopModeController>> desktopModeController) {
+        // Use optional-of-lazy for the dependency that this provider relies on.
+        // Lazy ensures that this provider will not be the cause the dependency is created
+        // when it will not be returned due to the condition below.
         if (DesktopModeStatus.IS_SUPPORTED) {
-            return desktopModeController;
+            return desktopModeController.map(Lazy::get);
         }
         return Optional.empty();
     }
@@ -711,10 +714,13 @@
 
     @WMSingleton
     @Provides
-    static Optional<DesktopModeTaskRepository> providesDesktopTaskRepository(
-            @DynamicOverride Optional<DesktopModeTaskRepository> desktopModeTaskRepository) {
+    static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(
+            @DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) {
+        // Use optional-of-lazy for the dependency that this provider relies on.
+        // Lazy ensures that this provider will not be the cause the dependency is created
+        // when it will not be returned due to the condition below.
         if (DesktopModeStatus.IS_SUPPORTED) {
-            return desktopModeTaskRepository;
+            return desktopModeTaskRepository.map(Lazy::get);
         }
         return Optional.empty();
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index f1670cd..6be8305 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -189,7 +189,7 @@
             ShellTaskOrganizer taskOrganizer,
             DisplayController displayController,
             SyncTransactionQueue syncQueue,
-            @DynamicOverride DesktopModeController desktopModeController) {
+            Optional<DesktopModeController> desktopModeController) {
         return new CaptionWindowDecorViewModel(
                     context,
                     mainHandler,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index abc4024..5824f51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -64,6 +64,7 @@
 
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -99,7 +100,9 @@
         mDesktopModeTaskRepository = desktopModeTaskRepository;
         mMainExecutor = mainExecutor;
         mSettingsObserver = new SettingsObserver(mContext, mainHandler);
-        shellInit.addInitCallback(this::onInit, this);
+        if (DesktopModeStatus.isSupported()) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
     }
 
     private void onInit() {
@@ -258,18 +261,36 @@
 
     @NonNull
     private WindowContainerTransaction bringDesktopAppsToFront() {
-        ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
-        ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
+
+        final List<RunningTaskInfo> taskInfos = new ArrayList<>();
         for (Integer taskId : activeTasks) {
             RunningTaskInfo taskInfo = mShellTaskOrganizer.getRunningTaskInfo(taskId);
             if (taskInfo != null) {
                 taskInfos.add(taskInfo);
             }
         }
-        // Order by lastActiveTime, descending
-        taskInfos.sort(Comparator.comparingLong(task -> -task.lastActiveTime));
-        WindowContainerTransaction wct = new WindowContainerTransaction();
+
+        if (taskInfos.isEmpty()) {
+            return wct;
+        }
+
+        final boolean allActiveTasksAreVisible = taskInfos.stream()
+                .allMatch(info -> mDesktopModeTaskRepository.isVisibleTask(info.taskId));
+        if (allActiveTasksAreVisible) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                    "bringDesktopAppsToFront: active tasks are already in front, skipping.");
+            return wct;
+        }
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                "bringDesktopAppsToFront: reordering all active tasks to the front");
+        final List<Integer> allTasksInZOrder =
+                mDesktopModeTaskRepository.getFreeformTasksInZOrder();
+        // Sort by z-order, bottom to top, so that the top-most task is reordered to the top last
+        // in the WCT.
+        taskInfos.sort(Comparator.comparingInt(task -> -allTasksInZOrder.indexOf(task.taskId)));
         for (RunningTaskInfo task : taskInfos) {
             wct.reorder(task.token, true);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 2fafe67..e3eb2b7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -37,6 +37,13 @@
             "persist.wm.debug.desktop_mode", false);
 
     /**
+     * Return {@code true} if desktop mode support is enabled
+     */
+    public static boolean isSupported() {
+        return IS_SUPPORTED;
+    }
+
+    /**
      * Check if desktop mode is active
      *
      * @return {@code true} if active
@@ -54,4 +61,5 @@
             return false;
         }
     }
+
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index b7749fc..600ccc1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -33,6 +33,8 @@
      */
     private val activeTasks = ArraySet<Int>()
     private val visibleTasks = ArraySet<Int>()
+    // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
+    private val freeformTasksInZOrder = mutableListOf<Int>()
     private val activeTasksListeners = ArraySet<ActiveTasksListener>()
     // Track visible tasks separately because a task may be part of the desktop but not visible.
     private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
@@ -101,6 +103,13 @@
     }
 
     /**
+     * Whether a task is visible.
+     */
+    fun isVisibleTask(taskId: Int): Boolean {
+        return visibleTasks.contains(taskId)
+    }
+
+    /**
      * Get a set of the active tasks
      */
     fun getActiveTasks(): ArraySet<Int> {
@@ -108,6 +117,13 @@
     }
 
     /**
+     * Get a list of freeform tasks, ordered from top-bottom (top at index 0).
+     */
+    fun getFreeformTasksInZOrder(): List<Int> {
+        return freeformTasksInZOrder
+    }
+
+    /**
      * Updates whether a freeform task with this id is visible or not and notifies listeners.
      */
     fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) {
@@ -127,6 +143,23 @@
     }
 
     /**
+     * Add (or move if it already exists) the task to the top of the ordered list.
+     */
+    fun addOrMoveFreeformTaskToTop(taskId: Int) {
+        if (freeformTasksInZOrder.contains(taskId)) {
+            freeformTasksInZOrder.remove(taskId)
+        }
+        freeformTasksInZOrder.add(0, taskId)
+    }
+
+    /**
+     * Remove the task from the ordered list.
+     */
+    fun removeFreeformTask(taskId: Int) {
+        freeformTasksInZOrder.remove(taskId)
+    }
+
+    /**
      * Defines interface for classes that can listen to changes for active tasks in desktop mode.
      */
     interface ActiveTasksListener {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
new file mode 100644
index 0000000..926cfb3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
@@ -0,0 +1,2 @@
+# WM shell sub-module desktop owners
+madym@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 44bcdb2..8a9b74f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -38,7 +38,8 @@
  * {@link ShellTaskOrganizer.TaskListener} for {@link
  * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}.
  */
-public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
+public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
+        ShellTaskOrganizer.FocusListener {
     private static final String TAG = "FreeformTaskListener";
 
     private final ShellTaskOrganizer mShellTaskOrganizer;
@@ -67,6 +68,9 @@
 
     private void onInit() {
         mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM);
+        if (DesktopModeStatus.IS_SUPPORTED) {
+            mShellTaskOrganizer.addFocusListener(this);
+        }
     }
 
     @Override
@@ -86,13 +90,16 @@
             t.apply();
         }
 
-        if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isVisible) {
+        if (DesktopModeStatus.IS_SUPPORTED) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
-                if (repository.addActiveTask(taskInfo.taskId)) {
-                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
-                            "Adding active freeform task: #%d", taskInfo.taskId);
+                repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
+                if (taskInfo.isVisible) {
+                    if (repository.addActiveTask(taskInfo.taskId)) {
+                        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+                                "Adding active freeform task: #%d", taskInfo.taskId);
+                    }
+                    repository.updateVisibleFreeformTasks(taskInfo.taskId, true);
                 }
-                repository.updateVisibleFreeformTasks(taskInfo.taskId, true);
             });
         }
     }
@@ -105,6 +112,7 @@
 
         if (DesktopModeStatus.IS_SUPPORTED) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
+                repository.removeFreeformTask(taskInfo.taskId);
                 if (repository.removeActiveTask(taskInfo.taskId)) {
                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                             "Removing active freeform task: #%d", taskInfo.taskId);
@@ -140,6 +148,18 @@
     }
 
     @Override
+    public void onFocusTaskChanged(RunningTaskInfo taskInfo) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG,
+                "Freeform Task Focus Changed: #%d focused=%b",
+                taskInfo.taskId, taskInfo.isFocused);
+        if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isFocused) {
+            mDesktopModeTaskRepository.ifPresent(repository -> {
+                repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
+            });
+        }
+    }
+
+    @Override
     public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
         b.setParent(findTaskSurface(taskId));
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS
new file mode 100644
index 0000000..0c2d5c4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS
@@ -0,0 +1,2 @@
+# WM shell sub-module freeform owners
+madym@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 7908f35..db0f0bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -377,6 +377,8 @@
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         pw.println(prefix + TAG);
+        pw.println(prefix + " mListener=" + mListener);
+        pw.println(prefix + "Tasks:");
         ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE,
                 ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser());
         for (int i = 0; i < recentTasks.size(); i++) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index d86aadc..2f2bc77 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -73,6 +73,9 @@
     /** Called when device waking up finished. */
     void onFinishedWakingUp();
 
+    /** Called when requested to go to fullscreen from the current active split app. */
+    void goToFullscreenFromSplit();
+
     /** Get a string representation of a stage type */
     static String stageTypeToString(@StageType int stage) {
         switch (stage) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index b20125d..9329d02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -123,6 +123,7 @@
     public static final int EXIT_REASON_SCREEN_LOCKED = 7;
     public static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8;
     public static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9;
+    public static final int EXIT_REASON_FULLSCREEN_SHORTCUT = 10;
     @IntDef(value = {
             EXIT_REASON_UNKNOWN,
             EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW,
@@ -134,6 +135,7 @@
             EXIT_REASON_SCREEN_LOCKED,
             EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP,
             EXIT_REASON_CHILD_TASK_ENTER_PIP,
+            EXIT_REASON_FULLSCREEN_SHORTCUT,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface ExitReason{}
@@ -418,6 +420,10 @@
         mStageCoordinator.unregisterSplitScreenListener(listener);
     }
 
+    public void goToFullscreenFromSplit() {
+        mStageCoordinator.goToFullscreenFromSplit();
+    }
+
     public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
         final int[] result = new int[1];
         IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
@@ -523,10 +529,24 @@
             @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
             InstanceId instanceId) {
         Intent fillInIntent = null;
-        if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
-                && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
-            fillInIntent = new Intent();
-            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+        if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)) {
+            if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+                fillInIntent = new Intent();
+                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+            } else {
+                try {
+                    adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */);
+                    ActivityTaskManager.getService().startActivityFromRecents(taskId, options2);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error starting remote animation", e);
+                }
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Cancel entering split as not supporting multi-instances");
+                Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+                        Toast.LENGTH_SHORT).show();
+                return;
+            }
         }
         mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent,
                 options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId);
@@ -536,10 +556,17 @@
             int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
             float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
         Intent fillInIntent = null;
-        if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
-                && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
-            fillInIntent = new Intent();
-            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+        if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)) {
+            if (supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+                fillInIntent = new Intent();
+                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+            } else {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Cancel entering split as not supporting multi-instances");
+                Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+                        Toast.LENGTH_SHORT).show();
+            }
         }
         mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId,
                 options2, splitPosition, splitRatio, remoteTransition, instanceId);
@@ -551,12 +578,26 @@
             float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
         Intent fillInIntent1 = null;
         Intent fillInIntent2 = null;
-        if (launchSameComponentAdjacently(pendingIntent1, pendingIntent2)
-                && supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) {
-            fillInIntent1 = new Intent();
-            fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
-            fillInIntent2 = new Intent();
-            fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+        if (launchSameComponentAdjacently(pendingIntent1, pendingIntent2)) {
+            if (supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) {
+                fillInIntent1 = new Intent();
+                fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                fillInIntent2 = new Intent();
+                fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+            } else {
+                try {
+                    adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */);
+                    pendingIntent1.send();
+                } catch (RemoteException | PendingIntent.CanceledException e) {
+                    Slog.e(TAG, "Error starting remote animation", e);
+                }
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Cancel entering split as not supporting multi-instances");
+                Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+                        Toast.LENGTH_SHORT).show();
+                return;
+            }
         }
         mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, options1,
                 pendingIntent2, fillInIntent2, options2, splitPosition, splitRatio, adapter,
@@ -595,6 +636,8 @@
                 mStageCoordinator.switchSplitPosition("startIntent");
                 return;
             } else {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                        "Cancel entering split as not supporting multi-instances");
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
                 return;
@@ -860,9 +903,12 @@
 
         @Override
         public void onFinishedWakingUp() {
-            mMainExecutor.execute(() -> {
-                SplitScreenController.this.onFinishedWakingUp();
-            });
+            mMainExecutor.execute(SplitScreenController.this::onFinishedWakingUp);
+        }
+
+        @Override
+        public void goToFullscreenFromSplit() {
+            mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit);
         }
     }
 
@@ -918,33 +964,25 @@
         @Override
         public void exitSplitScreen(int toTopTaskId) {
             executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
-                    (controller) -> {
-                        controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN);
-                    });
+                    (controller) -> controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN));
         }
 
         @Override
         public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
             executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide",
-                    (controller) -> {
-                        controller.exitSplitScreenOnHide(exitSplitScreenOnHide);
-                    });
+                    (controller) -> controller.exitSplitScreenOnHide(exitSplitScreenOnHide));
         }
 
         @Override
         public void removeFromSideStage(int taskId) {
             executeRemoteCallWithTaskPermission(mController, "removeFromSideStage",
-                    (controller) -> {
-                        controller.removeFromSideStage(taskId);
-                    });
+                    (controller) -> controller.removeFromSideStage(taskId));
         }
 
         @Override
         public void startTask(int taskId, int position, @Nullable Bundle options) {
             executeRemoteCallWithTaskPermission(mController, "startTask",
-                    (controller) -> {
-                        controller.startTask(taskId, position, options);
-                    });
+                    (controller) -> controller.startTask(taskId, position, options));
         }
 
         @Override
@@ -1036,19 +1074,16 @@
         public void startShortcut(String packageName, String shortcutId, int position,
                 @Nullable Bundle options, UserHandle user, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startShortcut",
-                    (controller) -> {
-                        controller.startShortcut(packageName, shortcutId, position, options, user,
-                                instanceId);
-                    });
+                    (controller) -> controller.startShortcut(packageName, shortcutId, position,
+                            options, user, instanceId));
         }
 
         @Override
         public void startIntent(PendingIntent intent, Intent fillInIntent, int position,
                 @Nullable Bundle options, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startIntent",
-                    (controller) -> {
-                        controller.startIntent(intent, fillInIntent, position, options, instanceId);
-                    });
+                    (controller) -> controller.startIntent(intent, fillInIntent, position, options,
+                            instanceId));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 21a1310..1cf3a89 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -47,6 +47,7 @@
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitDecorManager;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.transition.OneShotRemoteHandler;
 import com.android.wm.shell.transition.Transitions;
@@ -64,6 +65,7 @@
     DismissTransition mPendingDismiss = null;
     TransitSession mPendingEnter = null;
     TransitSession mPendingRecent = null;
+    TransitSession mPendingResize = null;
 
     private IBinder mAnimatingTransition = null;
     OneShotRemoteHandler mPendingRemoteHandler = null;
@@ -177,6 +179,43 @@
         onFinish(null /* wct */, null /* wctCB */);
     }
 
+    void applyResizeTransition(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
+            @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) {
+        mFinishCallback = finishCallback;
+        mAnimatingTransition = transition;
+        mFinishTransaction = finishTransaction;
+
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            if (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer())) {
+                final SurfaceControl leash = change.getLeash();
+                startTransaction.setPosition(leash, change.getEndAbsBounds().left,
+                        change.getEndAbsBounds().top);
+                startTransaction.setWindowCrop(leash, change.getEndAbsBounds().width(),
+                        change.getEndAbsBounds().height());
+
+                SplitDecorManager decor = mainRoot.equals(change.getContainer())
+                        ? mainDecor : sideDecor;
+                ValueAnimator va = new ValueAnimator();
+                mAnimations.add(va);
+                decor.setScreenshotIfNeeded(change.getSnapshot(), startTransaction);
+                decor.onResized(startTransaction, () -> {
+                    mTransitions.getMainExecutor().execute(() -> {
+                        mAnimations.remove(va);
+                        onFinish(null /* wct */, null /* wctCB */);
+                    });
+                });
+            }
+        }
+
+        startTransaction.apply();
+        onFinish(null /* wct */, null /* wctCB */);
+    }
+
     boolean isPendingTransition(IBinder transition) {
         return getPendingTransition(transition) != null;
     }
@@ -193,6 +232,10 @@
         return mPendingDismiss != null && mPendingDismiss.mTransition == transition;
     }
 
+    boolean isPendingResize(IBinder transition) {
+        return mPendingResize != null && mPendingResize.mTransition == transition;
+    }
+
     @Nullable
     private TransitSession getPendingTransition(IBinder transition) {
         if (isPendingEnter(transition)) {
@@ -201,11 +244,14 @@
             return mPendingRecent;
         } else if (isPendingDismiss(transition)) {
             return mPendingDismiss;
+        } else if (isPendingResize(transition)) {
+            return mPendingResize;
         }
 
         return null;
     }
 
+
     /** Starts a transition to enter split with a remote transition animator. */
     IBinder startEnterTransition(
             @WindowManager.TransitionType int transitType,
@@ -258,6 +304,21 @@
                 exitReasonToString(reason), stageTypeToString(dismissTop));
     }
 
+    IBinder startResizeTransition(WindowContainerTransaction wct,
+            Transitions.TransitionHandler handler,
+            @Nullable TransitionFinishedCallback finishCallback) {
+        IBinder transition = mTransitions.startTransition(TRANSIT_CHANGE, wct, handler);
+        setResizeTransition(transition, finishCallback);
+        return transition;
+    }
+
+    void setResizeTransition(@NonNull IBinder transition,
+            @Nullable TransitionFinishedCallback finishCallback) {
+        mPendingResize = new TransitSession(transition, null /* consumedCb */, finishCallback);
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  splitTransition "
+                + " deduced Resize split screen");
+    }
+
     void setRecentTransition(@NonNull IBinder transition,
             @Nullable RemoteTransition remoteTransition,
             @Nullable TransitionFinishedCallback finishCallback) {
@@ -324,6 +385,9 @@
             mPendingRecent.onConsumed(aborted);
             mPendingRecent = null;
             mPendingRemoteHandler = null;
+        } else if (isPendingResize(transition)) {
+            mPendingResize.onConsumed(aborted);
+            mPendingResize = null;
         }
     }
 
@@ -340,6 +404,9 @@
         } else if (isPendingDismiss(mAnimatingTransition)) {
             mPendingDismiss.onFinished(wct, mFinishTransaction);
             mPendingDismiss = null;
+        } else if (isPendingResize(mAnimatingTransition)) {
+            mPendingResize.onFinished(wct, mFinishTransaction);
+            mPendingResize = null;
         }
 
         mPendingRemoteHandler = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
index 2dc4a04..1016e1b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
@@ -23,6 +23,7 @@
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
@@ -38,6 +39,7 @@
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED;
@@ -180,6 +182,8 @@
                 return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
             case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
                 return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
+            case EXIT_REASON_FULLSCREEN_SHORTCUT:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
             case EXIT_REASON_UNKNOWN:
                 // Fall through
             default:
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index eb7b0d7..da8dc87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -49,6 +49,7 @@
 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
@@ -455,8 +456,6 @@
     void startIntentLegacy(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
             @Nullable Bundle options) {
         final boolean isEnteringSplit = !isSplitActive();
-        final WindowContainerTransaction evictWct = new WindowContainerTransaction();
-        prepareEvictChildTasks(position, evictWct);
 
         LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
             @Override
@@ -464,22 +463,21 @@
                     RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
                     IRemoteAnimationFinishedCallback finishedCallback,
                     SurfaceControl.Transaction t) {
-                if (isEnteringSplit) {
-                    boolean openingToSide = false;
-                    if (apps != null) {
-                        for (int i = 0; i < apps.length; ++i) {
-                            if (apps[i].mode == MODE_OPENING
-                                    && mSideStage.containsTask(apps[i].taskId)) {
-                                openingToSide = true;
-                                break;
-                            }
+                boolean openingToSide = false;
+                if (apps != null) {
+                    for (int i = 0; i < apps.length; ++i) {
+                        if (apps[i].mode == MODE_OPENING
+                                && mSideStage.containsTask(apps[i].taskId)) {
+                            openingToSide = true;
+                            break;
                         }
                     }
-                    if (!openingToSide) {
-                        mMainExecutor.execute(() -> exitSplitScreen(
-                                mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
-                                EXIT_REASON_UNKNOWN));
-                    }
+                }
+
+                if (isEnteringSplit && !openingToSide) {
+                    mMainExecutor.execute(() -> exitSplitScreen(
+                            mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
+                            EXIT_REASON_UNKNOWN));
                 }
 
                 if (apps != null) {
@@ -499,7 +497,12 @@
                     }
                 }
 
-                mSyncQueue.queue(evictWct);
+
+                if (!isEnteringSplit && openingToSide) {
+                    final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+                    prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+                    mSyncQueue.queue(evictWct);
+                }
             }
         };
 
@@ -1115,15 +1118,8 @@
      * Exits the split screen by finishing one of the tasks.
      */
     protected void exitStage(@SplitPosition int stageToClose) {
-        if (ENABLE_SHELL_TRANSITIONS) {
-            StageTaskListener stageToTop = mSideStagePosition == stageToClose
-                    ? mMainStage
-                    : mSideStage;
-            exitSplitScreen(stageToTop, EXIT_REASON_APP_FINISHED);
-        } else {
-            boolean toEnd = stageToClose == SPLIT_POSITION_BOTTOM_OR_RIGHT;
-            mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_APP_FINISHED);
-        }
+        mSplitLayout.flingDividerToDismiss(stageToClose == SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                EXIT_REASON_APP_FINISHED);
     }
 
     /**
@@ -1157,6 +1153,9 @@
             case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
                 // User has unlocked the device after folded
             case EXIT_REASON_DEVICE_FOLDED:
+                // The device is folded
+            case EXIT_REASON_FULLSCREEN_SHORTCUT:
+                // User has used a keyboard shortcut to go back to fullscreen from split
                 return true;
             default:
                 return false;
@@ -1670,15 +1669,29 @@
     public void onLayoutSizeChanged(SplitLayout layout) {
         // Reset this flag every time onLayoutSizeChanged.
         mShowDecorImmediately = false;
+
+        if (!ENABLE_SHELL_TRANSITIONS) {
+            // Only need screenshot for legacy case because shell transition should screenshot
+            // itself during transition.
+            final SurfaceControl.Transaction startT = mTransactionPool.acquire();
+            mMainStage.screenshotIfNeeded(startT);
+            mSideStage.screenshotIfNeeded(startT);
+            mTransactionPool.release(startT);
+        }
+
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         updateWindowBounds(layout, wct);
         sendOnBoundsChanged();
-        mSyncQueue.queue(wct);
-        mSyncQueue.runInSync(t -> {
-            updateSurfaceBounds(layout, t, false /* applyResizingOffset */);
-            mMainStage.onResized(t);
-            mSideStage.onResized(t);
-        });
+        if (ENABLE_SHELL_TRANSITIONS) {
+            mSplitTransitions.startResizeTransition(wct, this, null /* callback */);
+        } else {
+            mSyncQueue.queue(wct);
+            mSyncQueue.runInSync(t -> {
+                updateSurfaceBounds(layout, t, false /* applyResizingOffset */);
+                mMainStage.onResized(t);
+                mSideStage.onResized(t);
+            });
+        }
         mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
     }
 
@@ -2032,6 +2045,12 @@
         } else if (mSplitTransitions.isPendingDismiss(transition)) {
             shouldAnimate = startPendingDismissAnimation(
                     mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction);
+        } else if (mSplitTransitions.isPendingResize(transition)) {
+            mSplitTransitions.applyResizeTransition(transition, info, startTransaction,
+                    finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token,
+                    mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(),
+                    mSideStage.getSplitDecorManager());
+            return true;
         }
         if (!shouldAnimate) return false;
 
@@ -2119,6 +2138,16 @@
         return true;
     }
 
+    public void goToFullscreenFromSplit() {
+        boolean leftOrTop;
+        if (mSideStage.isFocused()) {
+            leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+        } else {
+            leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
+        }
+        mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT);
+    }
+
     /** Synchronize split-screen state with transition and make appropriate preparations. */
     public void prepareDismissAnimation(@StageType int toStage, @ExitReason int dismissReason,
             @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 358f712..8a52c87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -292,7 +292,13 @@
 
     void onResized(SurfaceControl.Transaction t) {
         if (mSplitDecorManager != null) {
-            mSplitDecorManager.onResized(t);
+            mSplitDecorManager.onResized(t, null);
+        }
+    }
+
+    void screenshotIfNeeded(SurfaceControl.Transaction t) {
+        if (mSplitDecorManager != null) {
+            mSplitDecorManager.screenshotIfNeeded(t);
         }
     }
 
@@ -304,6 +310,10 @@
         }
     }
 
+    SplitDecorManager getSplitDecorManager() {
+        return mSplitDecorManager;
+    }
+
     void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) {
         // Clear overridden bounds and windowing mode to make sure the child task can inherit
         // windowing mode and bounds from split root.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index e40db4e..5655402 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -55,6 +55,7 @@
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
 import com.android.wm.shell.transition.Transitions;
 
+import java.util.Optional;
 import java.util.function.Supplier;
 
 /**
@@ -74,11 +75,11 @@
     private final DisplayController mDisplayController;
     private final SyncTransactionQueue mSyncQueue;
     private FreeformTaskTransitionStarter mTransitionStarter;
-    private DesktopModeController mDesktopModeController;
-    private EventReceiver mEventReceiver;
-    private InputMonitor mInputMonitor;
+    private Optional<DesktopModeController> mDesktopModeController;
     private boolean mTransitionDragActive;
 
+    private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
+
     private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
     private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
     private EventReceiverFactory mEventReceiverFactory = new EventReceiverFactory();
@@ -90,7 +91,7 @@
             ShellTaskOrganizer taskOrganizer,
             DisplayController displayController,
             SyncTransactionQueue syncQueue,
-            DesktopModeController desktopModeController) {
+            Optional<DesktopModeController> desktopModeController) {
         this(
                 context,
                 mainHandler,
@@ -110,7 +111,7 @@
             ShellTaskOrganizer taskOrganizer,
             DisplayController displayController,
             SyncTransactionQueue syncQueue,
-            DesktopModeController desktopModeController,
+            Optional<DesktopModeController> desktopModeController,
             CaptionWindowDecoration.Factory captionWindowDecorFactory,
             Supplier<InputManager> inputManagerSupplier) {
 
@@ -150,8 +151,15 @@
     @Override
     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
         final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+
         if (decoration == null) return;
 
+        int oldDisplayId = decoration.mDisplay.getDisplayId();
+        if (taskInfo.displayId != oldDisplayId) {
+            removeTaskFromEventReceiver(oldDisplayId);
+            incrementEventReceiverTasks(taskInfo.displayId);
+        }
+
         decoration.relayout(taskInfo);
     }
 
@@ -195,6 +203,11 @@
         if (decoration == null) return;
 
         decoration.close();
+        int displayId = taskInfo.displayId;
+        if (mEventReceiversByDisplay.contains(displayId)) {
+            EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
+            removeTaskFromEventReceiver(displayId);
+        }
     }
 
     private class CaptionTouchEventListener implements
@@ -234,10 +247,10 @@
             } else if (id == R.id.caption_handle) {
                 decoration.createHandleMenu();
             } else if (id == R.id.desktop_button) {
-                mDesktopModeController.setDesktopModeActive(true);
+                mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
                 decoration.closeHandleMenu();
             } else if (id == R.id.fullscreen_button) {
-                mDesktopModeController.setDesktopModeActive(false);
+                mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false));
                 decoration.closeHandleMenu();
                 decoration.setButtonVisibility();
             }
@@ -292,9 +305,9 @@
          */
         private void handleEventForMove(MotionEvent e) {
             RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
-            int windowingMode = mDesktopModeController
-                    .getDisplayAreaWindowingMode(taskInfo.displayId);
-            if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
+            if (mDesktopModeController.isPresent()
+                    && mDesktopModeController.get().getDisplayAreaWindowingMode(taskInfo.displayId)
+                    == WINDOWING_MODE_FULLSCREEN) {
                 return;
             }
             switch (e.getActionMasked()) {
@@ -319,7 +332,7 @@
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     if (e.getRawY(dragPointerIdx) <= statusBarHeight
                             && DesktopModeStatus.isActive(mContext)) {
-                        mDesktopModeController.setDesktopModeActive(false);
+                        mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false));
                     }
                     break;
                 }
@@ -329,8 +342,12 @@
 
     // InputEventReceiver to listen for touch input outside of caption bounds
     class EventReceiver extends InputEventReceiver {
-        EventReceiver(InputChannel channel, Looper looper) {
+        private InputMonitor mInputMonitor;
+        private int mTasksOnDisplay;
+        EventReceiver(InputMonitor inputMonitor, InputChannel channel, Looper looper) {
             super(channel, looper);
+            mInputMonitor = inputMonitor;
+            mTasksOnDisplay = 1;
         }
 
         @Override
@@ -338,15 +355,62 @@
             boolean handled = false;
             if (event instanceof MotionEvent) {
                 handled = true;
-                CaptionWindowDecorViewModel.this.handleReceivedMotionEvent((MotionEvent) event);
+                CaptionWindowDecorViewModel.this
+                        .handleReceivedMotionEvent((MotionEvent) event, mInputMonitor);
             }
             finishInputEvent(event, handled);
         }
+
+        @Override
+        public void dispose() {
+            if (mInputMonitor != null) {
+                mInputMonitor.dispose();
+                mInputMonitor = null;
+            }
+            super.dispose();
+        }
+
+        private void incrementTaskNumber() {
+            mTasksOnDisplay++;
+        }
+
+        private void decrementTaskNumber() {
+            mTasksOnDisplay--;
+        }
+
+        private int getTasksOnDisplay() {
+            return mTasksOnDisplay;
+        }
+    }
+
+    /**
+     * Check if an EventReceiver exists on a particular display.
+     * If it does, increment its task count. Otherwise, create one for that display.
+     * @param displayId the display to check against
+     */
+    private void incrementEventReceiverTasks(int displayId) {
+        if (mEventReceiversByDisplay.contains(displayId)) {
+            EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
+            eventReceiver.incrementTaskNumber();
+        } else {
+            createInputChannel(displayId);
+        }
+    }
+
+    // If all tasks on this display are gone, we don't need to monitor its input.
+    private void removeTaskFromEventReceiver(int displayId) {
+        if (!mEventReceiversByDisplay.contains(displayId)) return;
+        EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
+        if (eventReceiver == null) return;
+        eventReceiver.decrementTaskNumber();
+        if (eventReceiver.getTasksOnDisplay() == 0) {
+            disposeInputChannel(displayId);
+        }
     }
 
     class EventReceiverFactory {
-        EventReceiver create(InputChannel channel, Looper looper) {
-            return new EventReceiver(channel, looper);
+        EventReceiver create(InputMonitor inputMonitor, InputChannel channel, Looper looper) {
+            return new EventReceiver(inputMonitor, channel, looper);
         }
     }
 
@@ -355,14 +419,14 @@
      *
      * @param ev the {@link MotionEvent} received by {@link EventReceiver}
      */
-    private void handleReceivedMotionEvent(MotionEvent ev) {
+    private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) {
         if (!DesktopModeStatus.isActive(mContext)) {
             handleCaptionThroughStatusBar(ev);
         }
         handleEventOutsideFocusedCaption(ev);
         // Prevent status bar from reacting to a caption drag.
         if (mTransitionDragActive && !DesktopModeStatus.isActive(mContext)) {
-            mInputMonitor.pilferPointers();
+            inputMonitor.pilferPointers();
         }
     }
 
@@ -381,6 +445,7 @@
         }
     }
 
+
     /**
      * Perform caption actions if not able to through normal means.
      * Turn on desktop mode if handle is dragged below status bar.
@@ -407,7 +472,7 @@
                     int statusBarHeight = mDisplayController
                             .getDisplayLayout(focusedDecor.mTaskInfo.displayId).stableInsets().top;
                     if (ev.getY() > statusBarHeight) {
-                        mDesktopModeController.setDesktopModeActive(true);
+                        mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
                         return;
                     }
                 }
@@ -434,6 +499,22 @@
         return focusedDecor;
     }
 
+    private void createInputChannel(int displayId) {
+        InputManager inputManager = mInputManagerSupplier.get();
+        InputMonitor inputMonitor =
+                inputManager.monitorGestureInput("caption-touch", mContext.getDisplayId());
+        EventReceiver eventReceiver = mEventReceiverFactory.create(
+                inputMonitor, inputMonitor.getInputChannel(), Looper.myLooper());
+        mEventReceiversByDisplay.put(displayId, eventReceiver);
+    }
+
+    private void disposeInputChannel(int displayId) {
+        EventReceiver eventReceiver = mEventReceiversByDisplay.removeReturnOld(displayId);
+        if (eventReceiver != null) {
+            eventReceiver.dispose();
+        }
+    }
+
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
         if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
         return DesktopModeStatus.IS_SUPPORTED
@@ -472,14 +553,7 @@
         windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
         windowDecoration.setDragResizeCallback(taskPositioner);
         windowDecoration.relayout(taskInfo, startT, finishT);
-        if (mInputMonitor == null) {
-            InputManager inputManager = mInputManagerSupplier.get();
-            mInputMonitor =
-                    inputManager.monitorGestureInput("caption-touch", mContext.getDisplayId());
-            mEventReceiver =
-                    mEventReceiverFactory.create(
-                            mInputMonitor.getInputChannel(), Looper.myLooper());
-        }
+        incrementEventReceiverTasks(taskInfo.displayId);
     }
 
     private class DragStartListenerImpl implements TaskPositioner.DragStartListener {
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
index 08913c6..27fc381a 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
@@ -19,6 +19,8 @@
         <option name="run-command" value="locksettings set-disabled false" />
         <!-- restart launcher to activate TAPL -->
         <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
+        <!-- Ensure output directory is empty at the start -->
+        <option name="run-command" value="rm -rf /sdcard/flicker" />
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
index 6370df4..8465678 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
@@ -20,10 +20,10 @@
 import android.platform.test.annotations.Presubmit
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.FlickerBuilderProvider
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.entireScreenCovered
+import com.android.server.wm.flicker.junit.FlickerBuilderProvider
 import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd
 import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd
 import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible
@@ -45,12 +45,12 @@
 abstract class BaseTest
 @JvmOverloads
 constructor(
-    protected val testSpec: FlickerTestParameter,
+    protected val flicker: FlickerTest,
     protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
     protected val tapl: LauncherInstrumentation = LauncherInstrumentation()
 ) {
     init {
-        testSpec.setIsTablet(
+        flicker.scenario.setIsTablet(
             WindowManagerStateHelper(instrumentation, clearCacheAfterParsing = false)
                 .currentState
                 .wmState
@@ -68,13 +68,13 @@
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
-            setup { testSpec.setIsTablet(wmHelper.currentState.wmState.isTablet) }
+            setup { flicker.scenario.setIsTablet(wmHelper.currentState.wmState.isTablet) }
             transition()
         }
     }
 
     /** Checks that all parts of the screen are covered during the transition */
-    @Presubmit @Test open fun entireScreenCovered() = testSpec.entireScreenCovered()
+    @Presubmit @Test open fun entireScreenCovered() = flicker.entireScreenCovered()
 
     /**
      * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition
@@ -82,8 +82,8 @@
     @Presubmit
     @Test
     open fun navBarLayerIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarLayerIsVisibleAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerIsVisibleAtStartAndEnd()
     }
 
     /**
@@ -93,8 +93,8 @@
     @Presubmit
     @Test
     open fun navBarLayerPositionAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarLayerPositionAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerPositionAtStartAndEnd()
     }
 
     /**
@@ -105,8 +105,8 @@
     @Presubmit
     @Test
     open fun navBarWindowIsAlwaysVisible() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarWindowIsAlwaysVisible()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsAlwaysVisible()
     }
 
     /**
@@ -115,8 +115,8 @@
     @Presubmit
     @Test
     open fun taskBarLayerIsVisibleAtStartAndEnd() {
-        Assume.assumeTrue(testSpec.isTablet)
-        testSpec.taskBarLayerIsVisibleAtStartAndEnd()
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.taskBarLayerIsVisibleAtStartAndEnd()
     }
 
     /**
@@ -127,8 +127,8 @@
     @Presubmit
     @Test
     open fun taskBarWindowIsAlwaysVisible() {
-        Assume.assumeTrue(testSpec.isTablet)
-        testSpec.taskBarWindowIsAlwaysVisible()
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.taskBarWindowIsAlwaysVisible()
     }
 
     /**
@@ -137,8 +137,7 @@
      */
     @Presubmit
     @Test
-    open fun statusBarLayerIsVisibleAtStartAndEnd() =
-        testSpec.statusBarLayerIsVisibleAtStartAndEnd()
+    open fun statusBarLayerIsVisibleAtStartAndEnd() = flicker.statusBarLayerIsVisibleAtStartAndEnd()
 
     /**
      * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
@@ -146,7 +145,7 @@
      */
     @Presubmit
     @Test
-    open fun statusBarLayerPositionAtStartAndEnd() = testSpec.statusBarLayerPositionAtStartAndEnd()
+    open fun statusBarLayerPositionAtStartAndEnd() = flicker.statusBarLayerPositionAtStartAndEnd()
 
     /**
      * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole
@@ -154,7 +153,7 @@
      */
     @Presubmit
     @Test
-    open fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    open fun statusBarWindowIsAlwaysVisible() = flicker.statusBarWindowIsAlwaysVisible()
 
     /**
      * Checks that all layers that are visible on the trace, are visible for at least 2 consecutive
@@ -163,7 +162,7 @@
     @Presubmit
     @Test
     open fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry() }
+        flicker.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry() }
     }
 
     /**
@@ -173,6 +172,6 @@
     @Presubmit
     @Test
     open fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertWm { this.visibleWindowsShownMoreThanOneConsecutiveEntry() }
+        flicker.assertWm { this.visibleWindowsShownMoreThanOneConsecutiveEntry() }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
index 8765ad1..5186914 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -18,23 +18,23 @@
 
 package com.android.wm.shell.flicker
 
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject
 import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
 import com.android.server.wm.traces.common.IComponentMatcher
 import com.android.server.wm.traces.common.region.Region
+import com.android.server.wm.traces.common.service.PlatformConsts
 
-fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() {
+fun FlickerTest.appPairsDividerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTestParameter.appPairsDividerIsInvisibleAtEnd() {
+fun FlickerTest.appPairsDividerIsInvisibleAtEnd() {
     assertLayersEnd { this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTestParameter.appPairsDividerBecomesVisible() {
+fun FlickerTest.appPairsDividerBecomesVisible() {
     assertLayers {
         this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
@@ -42,7 +42,7 @@
     }
 }
 
-fun FlickerTestParameter.splitScreenEntered(
+fun FlickerTest.splitScreenEntered(
     component1: IComponentMatcher,
     component2: IComponentMatcher,
     fromOtherApp: Boolean,
@@ -69,7 +69,7 @@
     splitScreenDividerIsVisibleAtEnd()
 }
 
-fun FlickerTestParameter.splitScreenDismissed(
+fun FlickerTest.splitScreenDismissed(
     component1: IComponentMatcher,
     component2: IComponentMatcher,
     toHome: Boolean
@@ -87,27 +87,27 @@
     splitScreenDividerIsInvisibleAtEnd()
 }
 
-fun FlickerTestParameter.splitScreenDividerIsVisibleAtStart() {
+fun FlickerTest.splitScreenDividerIsVisibleAtStart() {
     assertLayersStart { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTestParameter.splitScreenDividerIsVisibleAtEnd() {
+fun FlickerTest.splitScreenDividerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTestParameter.splitScreenDividerIsInvisibleAtStart() {
+fun FlickerTest.splitScreenDividerIsInvisibleAtStart() {
     assertLayersStart { this.isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTestParameter.splitScreenDividerIsInvisibleAtEnd() {
+fun FlickerTest.splitScreenDividerIsInvisibleAtEnd() {
     assertLayersEnd { this.isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTestParameter.splitScreenDividerBecomesVisible() {
+fun FlickerTest.splitScreenDividerBecomesVisible() {
     layerBecomesVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
 }
 
-fun FlickerTestParameter.splitScreenDividerBecomesInvisible() {
+fun FlickerTest.splitScreenDividerBecomesInvisible() {
     assertLayers {
         this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
             .then()
@@ -115,23 +115,23 @@
     }
 }
 
-fun FlickerTestParameter.layerBecomesVisible(component: IComponentMatcher) {
+fun FlickerTest.layerBecomesVisible(component: IComponentMatcher) {
     assertLayers { this.isInvisible(component).then().isVisible(component) }
 }
 
-fun FlickerTestParameter.layerBecomesInvisible(component: IComponentMatcher) {
+fun FlickerTest.layerBecomesInvisible(component: IComponentMatcher) {
     assertLayers { this.isVisible(component).then().isInvisible(component) }
 }
 
-fun FlickerTestParameter.layerIsVisibleAtEnd(component: IComponentMatcher) {
+fun FlickerTest.layerIsVisibleAtEnd(component: IComponentMatcher) {
     assertLayersEnd { this.isVisible(component) }
 }
 
-fun FlickerTestParameter.layerKeepVisible(component: IComponentMatcher) {
+fun FlickerTest.layerKeepVisible(component: IComponentMatcher) {
     assertLayers { this.isVisible(component) }
 }
 
-fun FlickerTestParameter.splitAppLayerBoundsBecomesVisible(
+fun FlickerTest.splitAppLayerBoundsBecomesVisible(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
@@ -145,12 +145,12 @@
                 component,
                 landscapePosLeft,
                 portraitPosTop,
-                endRotation
+                scenario.endRotation
             )
     }
 }
 
-fun FlickerTestParameter.splitAppLayerBoundsBecomesVisibleByDrag(component: IComponentMatcher) {
+fun FlickerTest.splitAppLayerBoundsBecomesVisibleByDrag(component: IComponentMatcher) {
     assertLayers {
         this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component), isOptional = true)
             .then()
@@ -161,7 +161,7 @@
     }
 }
 
-fun FlickerTestParameter.splitAppLayerBoundsBecomesInvisible(
+fun FlickerTest.splitAppLayerBoundsBecomesInvisible(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
@@ -171,7 +171,7 @@
                 component,
                 landscapePosLeft,
                 portraitPosTop,
-                endRotation
+                scenario.endRotation
             )
             .then()
             .isVisible(component, true)
@@ -180,27 +180,37 @@
     }
 }
 
-fun FlickerTestParameter.splitAppLayerBoundsIsVisibleAtEnd(
+fun FlickerTest.splitAppLayerBoundsIsVisibleAtEnd(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
 ) {
     assertLayersEnd {
-        splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, endRotation)
+        splitAppLayerBoundsSnapToDivider(
+            component,
+            landscapePosLeft,
+            portraitPosTop,
+            scenario.endRotation
+        )
     }
 }
 
-fun FlickerTestParameter.splitAppLayerBoundsKeepVisible(
+fun FlickerTest.splitAppLayerBoundsKeepVisible(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
 ) {
     assertLayers {
-        splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, endRotation)
+        splitAppLayerBoundsSnapToDivider(
+            component,
+            landscapePosLeft,
+            portraitPosTop,
+            scenario.endRotation
+        )
     }
 }
 
-fun FlickerTestParameter.splitAppLayerBoundsChanges(
+fun FlickerTest.splitAppLayerBoundsChanges(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
@@ -211,14 +221,14 @@
                 component,
                 landscapePosLeft,
                 portraitPosTop,
-                endRotation
+                scenario.endRotation
             )
         } else {
             this.splitAppLayerBoundsSnapToDivider(
                     component,
                     landscapePosLeft,
                     portraitPosTop,
-                    endRotation
+                    scenario.endRotation
                 )
                 .then()
                 .isInvisible(component)
@@ -227,7 +237,7 @@
                     component,
                     landscapePosLeft,
                     portraitPosTop,
-                    endRotation
+                    scenario.endRotation
                 )
         }
     }
@@ -237,7 +247,7 @@
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean,
-    rotation: Int
+    rotation: PlatformConsts.Rotation
 ): LayersTraceSubject {
     return invoke("splitAppLayerBoundsSnapToDivider") {
         it.splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, rotation)
@@ -248,7 +258,7 @@
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean,
-    rotation: Int
+    rotation: PlatformConsts.Rotation
 ): LayerTraceEntrySubject {
     val displayBounds = WindowUtils.getDisplayBounds(rotation)
     return invoke {
@@ -292,7 +302,7 @@
     }
 }
 
-fun FlickerTestParameter.appWindowBecomesVisible(component: IComponentMatcher) {
+fun FlickerTest.appWindowBecomesVisible(component: IComponentMatcher) {
     assertWm {
         this.isAppWindowInvisible(component)
             .then()
@@ -304,39 +314,39 @@
     }
 }
 
-fun FlickerTestParameter.appWindowBecomesInvisible(component: IComponentMatcher) {
+fun FlickerTest.appWindowBecomesInvisible(component: IComponentMatcher) {
     assertWm { this.isAppWindowVisible(component).then().isAppWindowInvisible(component) }
 }
 
-fun FlickerTestParameter.appWindowIsVisibleAtStart(component: IComponentMatcher) {
+fun FlickerTest.appWindowIsVisibleAtStart(component: IComponentMatcher) {
     assertWmStart { this.isAppWindowVisible(component) }
 }
 
-fun FlickerTestParameter.appWindowIsVisibleAtEnd(component: IComponentMatcher) {
+fun FlickerTest.appWindowIsVisibleAtEnd(component: IComponentMatcher) {
     assertWmEnd { this.isAppWindowVisible(component) }
 }
 
-fun FlickerTestParameter.appWindowIsInvisibleAtStart(component: IComponentMatcher) {
+fun FlickerTest.appWindowIsInvisibleAtStart(component: IComponentMatcher) {
     assertWmStart { this.isAppWindowInvisible(component) }
 }
 
-fun FlickerTestParameter.appWindowIsInvisibleAtEnd(component: IComponentMatcher) {
+fun FlickerTest.appWindowIsInvisibleAtEnd(component: IComponentMatcher) {
     assertWmEnd { this.isAppWindowInvisible(component) }
 }
 
-fun FlickerTestParameter.appWindowIsNotContainAtStart(component: IComponentMatcher) {
+fun FlickerTest.appWindowIsNotContainAtStart(component: IComponentMatcher) {
     assertWmStart { this.notContains(component) }
 }
 
-fun FlickerTestParameter.appWindowKeepVisible(component: IComponentMatcher) {
+fun FlickerTest.appWindowKeepVisible(component: IComponentMatcher) {
     assertWm { this.isAppWindowVisible(component) }
 }
 
-fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() {
+fun FlickerTest.dockedStackDividerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTestParameter.dockedStackDividerBecomesVisible() {
+fun FlickerTest.dockedStackDividerBecomesVisible() {
     assertLayers {
         this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
@@ -344,7 +354,7 @@
     }
 }
 
-fun FlickerTestParameter.dockedStackDividerBecomesInvisible() {
+fun FlickerTest.dockedStackDividerBecomesInvisible() {
     assertLayers {
         this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
@@ -352,12 +362,12 @@
     }
 }
 
-fun FlickerTestParameter.dockedStackDividerNotExistsAtEnd() {
+fun FlickerTest.dockedStackDividerNotExistsAtEnd() {
     assertLayersEnd { this.notContains(DOCKED_STACK_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd(
-    rotation: Int,
+fun FlickerTest.appPairsPrimaryBoundsIsVisibleAtEnd(
+    rotation: PlatformConsts.Rotation,
     primaryComponent: IComponentMatcher
 ) {
     assertLayersEnd {
@@ -366,8 +376,8 @@
     }
 }
 
-fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd(
-    rotation: Int,
+fun FlickerTest.dockedStackPrimaryBoundsIsVisibleAtEnd(
+    rotation: PlatformConsts.Rotation,
     primaryComponent: IComponentMatcher
 ) {
     assertLayersEnd {
@@ -376,8 +386,8 @@
     }
 }
 
-fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd(
-    rotation: Int,
+fun FlickerTest.appPairsSecondaryBoundsIsVisibleAtEnd(
+    rotation: PlatformConsts.Rotation,
     secondaryComponent: IComponentMatcher
 ) {
     assertLayersEnd {
@@ -386,8 +396,8 @@
     }
 }
 
-fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd(
-    rotation: Int,
+fun FlickerTest.dockedStackSecondaryBoundsIsVisibleAtEnd(
+    rotation: PlatformConsts.Rotation,
     secondaryComponent: IComponentMatcher
 ) {
     assertLayersEnd {
@@ -396,38 +406,38 @@
     }
 }
 
-fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region {
+fun getPrimaryRegion(dividerRegion: Region, rotation: PlatformConsts.Rotation): Region {
     val displayBounds = WindowUtils.getDisplayBounds(rotation)
-    return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
-        Region.from(
-            0,
-            0,
-            displayBounds.bounds.right,
-            dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset
-        )
-    } else {
+    return if (rotation.isRotated()) {
         Region.from(
             0,
             0,
             dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset,
             displayBounds.bounds.bottom
         )
+    } else {
+        Region.from(
+            0,
+            0,
+            displayBounds.bounds.right,
+            dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset
+        )
     }
 }
 
-fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region {
+fun getSecondaryRegion(dividerRegion: Region, rotation: PlatformConsts.Rotation): Region {
     val displayBounds = WindowUtils.getDisplayBounds(rotation)
-    return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+    return if (rotation.isRotated()) {
         Region.from(
+            dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset,
             0,
-            dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
             displayBounds.bounds.right,
             displayBounds.bounds.bottom
         )
     } else {
         Region.from(
-            dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset,
             0,
+            dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
             displayBounds.bounds.right,
             displayBounds.bounds.bottom
         )
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
index 0fc2004..996b677 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
@@ -21,21 +21,21 @@
 import android.content.Context
 import android.content.pm.PackageManager
 import android.os.ServiceManager
-import android.view.Surface
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiObject2
 import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.Flicker
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.IFlickerTestData
 import com.android.server.wm.flicker.helpers.LaunchBubbleHelper
 import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.BaseTest
 import org.junit.runners.Parameterized
 
 /** Base configurations for Bubble flicker tests */
-abstract class BaseBubbleScreen(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+abstract class BaseBubbleScreen(flicker: FlickerTest) : BaseTest(flicker) {
 
     protected val context: Context = instrumentation.context
     protected val testApp = LaunchBubbleHelper(instrumentation)
@@ -79,17 +79,18 @@
         }
     }
 
-    protected fun Flicker.waitAndGetAddBubbleBtn(): UiObject2? =
+    protected fun IFlickerTestData.waitAndGetAddBubbleBtn(): UiObject2? =
         device.wait(Until.findObject(By.text("Add Bubble")), FIND_OBJECT_TIMEOUT)
-    protected fun Flicker.waitAndGetCancelAllBtn(): UiObject2? =
+    protected fun IFlickerTestData.waitAndGetCancelAllBtn(): UiObject2? =
         device.wait(Until.findObject(By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT)
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
 
         const val FIND_OBJECT_TIMEOUT = 2000L
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
index ab72117..7fc12f0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
@@ -25,9 +25,9 @@
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -45,7 +45,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+open class DismissBubbleScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
 
     private val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
     private val displaySize = DisplayMetrics()
@@ -72,7 +72,7 @@
     @Presubmit
     @Test
     open fun testAppIsAlwaysVisible() {
-        testSpec.assertLayers { this.isVisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp) }
     }
 
     /** {@inheritDoc} */
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
index 226eab8..0cda626 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
@@ -20,9 +20,9 @@
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -42,7 +42,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+open class ExpandBubbleScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -64,6 +64,6 @@
     @Presubmit
     @Test
     open fun testAppIsAlwaysVisible() {
-        testSpec.assertLayers { this.isVisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp) }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
index 47167b8..04b1bdd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
@@ -23,9 +23,9 @@
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -43,7 +43,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class LaunchBubbleFromLockScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+class LaunchBubbleFromLockScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -88,7 +88,7 @@
     @FlakyTest(bugId = 242088970)
     @Test
     fun testAppIsVisibleAtEnd() {
-        testSpec.assertLayersEnd { this.isVisible(testApp) }
+        flicker.assertLayersEnd { this.isVisible(testApp) }
     }
 
     /** {@inheritDoc} */
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
index b865999..9b4e39c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
@@ -19,9 +19,9 @@
 import android.platform.test.annotations.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -40,7 +40,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+open class LaunchBubbleScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -59,6 +59,6 @@
 
     @Test
     open fun testAppIsAlwaysVisible() {
-        testSpec.assertLayers { this.isVisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp) }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
index bf4d7d4..b3a2ad3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
@@ -22,10 +22,10 @@
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiObject2
 import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
@@ -45,7 +45,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+open class MultiBubblesScreen(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
 
     @Before
     open fun before() {
@@ -87,6 +87,6 @@
     @Presubmit
     @Test
     open fun testAppIsAlwaysVisible() {
-        testSpec.assertLayers { this.isVisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp) }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt
index 57adeab..191f4fa 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreenShellTransit.kt
@@ -18,9 +18,9 @@
 
 import android.platform.test.annotations.FlakyTest
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Assume
 import org.junit.Before
 import org.junit.runner.RunWith
@@ -30,8 +30,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FlakyTest(bugId = 217777115)
-class MultiBubblesScreenShellTransit(testSpec: FlickerTestParameter) :
-    MultiBubblesScreen(testSpec) {
+class MultiBubblesScreenShellTransit(flicker: FlickerTest) : MultiBubblesScreen(flicker) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index 7546a55..5e898e8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -18,15 +18,15 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -59,7 +59,7 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @FlakyTest(bugId = 238367575)
-class AutoEnterPipOnGoToHomeTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) {
+class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipTest(flicker) {
     /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -73,7 +73,7 @@
                 // close gracefully so that onActivityUnpinned() can be called before force exit
                 pipApp.closePipWindow(wmHelper)
 
-                setRotation(Surface.ROTATION_0)
+                setRotation(PlatformConsts.Rotation.ROTATION_0)
                 RemoveAllTasksButHomeRule.removeAllTasksButHome()
                 pipApp.exit(wmHelper)
             }
@@ -83,7 +83,7 @@
     @FlakyTest(bugId = 256863309)
     @Test
     override fun pipLayerReduces() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
             pipLayerList.zipWithNext { previous, current ->
                 current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
@@ -96,8 +96,8 @@
     @Test
     fun pipLayerMovesTowardsRightBottomCorner() {
         // in gestural nav the swipe makes PiP first go upwards
-        Assume.assumeFalse(testSpec.isGesturalNavigation)
-        testSpec.assertLayers {
+        Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+        flicker.assertLayers {
             val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
             // Pip animates towards the right bottom corner, but because it is being resized at the
             // same time, it is possible it shrinks first quickly below the default position and get
@@ -112,7 +112,7 @@
     @Test
     override fun focusChanges() {
         // in gestural nav the focus goes to different activity on swipe up
-        Assume.assumeFalse(testSpec.isGesturalNavigation)
+        Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
         super.focusChanges()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
index c8aa6d2..79feeaa 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
@@ -17,14 +17,14 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -56,7 +56,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) {
+class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTest(flicker) {
     /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -68,7 +68,7 @@
                 pipApp.enableEnterPipOnUserLeaveHint()
             }
             teardown {
-                setRotation(Surface.ROTATION_0)
+                setRotation(PlatformConsts.Rotation.ROTATION_0)
                 RemoveAllTasksButHomeRule.removeAllTasksButHome()
                 pipApp.exit(wmHelper)
             }
@@ -78,10 +78,10 @@
     @Presubmit
     @Test
     override fun pipAppLayerAlwaysVisible() {
-        if (!testSpec.isGesturalNavigation) super.pipAppLayerAlwaysVisible()
+        if (!flicker.scenario.isGesturalNavigation) super.pipAppLayerAlwaysVisible()
         else {
             // pip layer in gesture nav will disappear during transition
-            testSpec.assertLayers {
+            flicker.assertLayers {
                 this.isVisible(pipApp).then().isInvisible(pipApp).then().isVisible(pipApp)
             }
         }
@@ -91,7 +91,7 @@
     @Test
     override fun pipLayerReduces() {
         // in gestural nav the pip enters through alpha animation
-        Assume.assumeFalse(testSpec.isGesturalNavigation)
+        Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
         super.pipLayerReduces()
     }
 
@@ -99,7 +99,7 @@
     @Test
     override fun focusChanges() {
         // in gestural nav the focus goes to different activity on swipe up
-        Assume.assumeFalse(testSpec.isGesturalNavigation)
+        Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
         super.focusChanges()
     }
 
@@ -112,11 +112,11 @@
     @Presubmit
     @Test
     override fun pipLayerRemainInsideVisibleBounds() {
-        if (!testSpec.isGesturalNavigation) super.pipLayerRemainInsideVisibleBounds()
+        if (!flicker.scenario.isGesturalNavigation) super.pipLayerRemainInsideVisibleBounds()
         else {
             // pip layer in gesture nav will disappear during transition
-            testSpec.assertLayersStart { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
-            testSpec.assertLayersEnd { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
+            flicker.assertLayersStart { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
+            flicker.assertLayersEnd { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
index 2b629e7..1a76142 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
@@ -17,16 +17,16 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -57,7 +57,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+open class EnterPipTest(flicker: FlickerTest) : PipTransition(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -68,7 +68,7 @@
                 pipApp.launchViaIntent(wmHelper)
             }
             teardown {
-                setRotation(Surface.ROTATION_0)
+                setRotation(PlatformConsts.Rotation.ROTATION_0)
                 RemoveAllTasksButHomeRule.removeAllTasksButHome()
                 pipApp.exit(wmHelper)
             }
@@ -79,16 +79,14 @@
     @Presubmit
     @Test
     open fun pipAppWindowAlwaysVisible() {
-        testSpec.assertWm { this.isAppWindowVisible(pipApp) }
+        flicker.assertWm { this.isAppWindowVisible(pipApp) }
     }
 
-    /**
-     * Checks [pipApp] layer remains visible throughout the animation
-     */
+    /** Checks [pipApp] layer remains visible throughout the animation */
     @Presubmit
     @Test
     open fun pipAppLayerAlwaysVisible() {
-        testSpec.assertLayers { this.isVisible(pipApp) }
+        flicker.assertLayers { this.isVisible(pipApp) }
     }
 
     /**
@@ -98,7 +96,7 @@
     @Presubmit
     @Test
     fun pipWindowRemainInsideVisibleBounds() {
-        testSpec.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+        flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
     /**
@@ -108,14 +106,14 @@
     @Presubmit
     @Test
     open fun pipLayerRemainInsideVisibleBounds() {
-        testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+        flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
     /** Checks that the visible region of [pipApp] always reduces during the animation */
     @Presubmit
     @Test
     open fun pipLayerReduces() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
             pipLayerList.zipWithNext { previous, current ->
                 current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
@@ -127,7 +125,7 @@
     @Presubmit
     @Test
     fun pipWindowBecomesPinned() {
-        testSpec.assertWm {
+        flicker.assertWm {
             invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp) }
                 .then()
                 .invoke("pipWindowIsPinned") { it.isPinned(pipApp) }
@@ -138,7 +136,7 @@
     @Presubmit
     @Test
     fun launcherLayerBecomesVisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             isInvisible(ComponentNameMatcher.LAUNCHER)
                 .then()
                 .isVisible(ComponentNameMatcher.LAUNCHER)
@@ -152,21 +150,22 @@
     @Presubmit
     @Test
     open fun focusChanges() {
-        testSpec.assertEventLog { this.focusChanges(pipApp.`package`, "NexusLauncherActivity") }
+        flicker.assertEventLog { this.focusChanges(pipApp.`package`, "NexusLauncherActivity") }
     }
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+         * and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
index b4594de..2b90243 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
@@ -19,22 +19,22 @@
 import android.app.Activity
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
 import com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_ENTER_PIP
 import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
 import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT
 import org.junit.Assume
@@ -51,32 +51,31 @@
  * To run this test: `atest WMShellFlickerTests:EnterPipToOtherOrientationTest`
  *
  * Actions:
+ * ```
  *     Launch [testApp] on a fixed portrait orientation
  *     Launch [pipApp] on a fixed landscape orientation
  *     Broadcast action [ACTION_ENTER_PIP] to enter pip mode
- *
+ * ```
  * Notes:
+ * ```
  *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
  *        are inherited [PipTransition]
  *     2. Part of the test setup occurs automatically via
  *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
  *        including configuring navigation mode, initial orientation and ensuring no
  *        apps are running before setup
+ * ```
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterPipToOtherOrientationTest(
-    testSpec: FlickerTestParameter
-) : PipTransition(testSpec) {
+class EnterPipToOtherOrientationTest(flicker: FlickerTest) : PipTransition(flicker) {
     private val testApp = FixedOrientationAppHelper(instrumentation)
-    private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90)
-    private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0)
+    private val startingBounds = WindowUtils.getDisplayBounds(PlatformConsts.Rotation.ROTATION_90)
+    private val endingBounds = WindowUtils.getDisplayBounds(PlatformConsts.Rotation.ROTATION_0)
 
-    /**
-     * Defines the transition used to run the test
-     */
+    /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             setup {
@@ -85,19 +84,18 @@
 
                 // Launch a portrait only app on the fullscreen stack
                 testApp.launchViaIntent(
-                    wmHelper, stringExtras = mapOf(
-                        EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString()
-                    )
+                    wmHelper,
+                    stringExtras = mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())
                 )
                 // Launch the PiP activity fixed as landscape
                 pipApp.launchViaIntent(
-                    wmHelper, stringExtras = mapOf(
-                        EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()
-                    )
+                    wmHelper,
+                    stringExtras =
+                        mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())
                 )
             }
             teardown {
-                setRotation(Surface.ROTATION_0)
+                setRotation(PlatformConsts.Rotation.ROTATION_0)
                 RemoveAllTasksButHomeRule.removeAllTasksButHome()
                 pipApp.exit(wmHelper)
                 testApp.exit(wmHelper)
@@ -107,7 +105,8 @@
                 // in portrait
                 broadcastActionTrigger.doAction(ACTION_ENTER_PIP)
                 // during rotation the status bar becomes invisible and reappears at the end
-                wmHelper.StateSyncBuilder()
+                wmHelper
+                    .StateSyncBuilder()
                     .withPipShown()
                     .withNavOrTaskBarVisible()
                     .withStatusBarVisible()
@@ -116,21 +115,21 @@
         }
 
     /**
-     * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation]
-     * to fix a orientation, Tablets instead keep the same orientation and add letterboxes
+     * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation] to
+     * fix a orientation, Tablets instead keep the same orientation and add letterboxes
      */
     @Before
     fun setup() {
-        Assume.assumeFalse(testSpec.isTablet)
+        Assume.assumeFalse(flicker.scenario.isTablet)
     }
 
     /**
-     * Checks that the [ComponentNameMatcher.NAV_BAR] has the correct position at
-     * the start and end of the transition
+     * Checks that the [ComponentNameMatcher.NAV_BAR] has the correct position at the start and end
+     * of the transition
      */
     @FlakyTest
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() = testSpec.navBarLayerPositionAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = flicker.navBarLayerPositionAtStartAndEnd()
 
     /**
      * Checks that all parts of the screen are covered at the start and end of the transition
@@ -139,7 +138,7 @@
      */
     @Presubmit
     @Test
-    fun entireScreenCoveredAtStartAndEnd() = testSpec.entireScreenCovered(allStates = false)
+    fun entireScreenCoveredAtStartAndEnd() = flicker.entireScreenCovered(allStates = false)
 
     @FlakyTest(bugId = 251219769)
     @Test
@@ -147,89 +146,65 @@
         super.entireScreenCovered()
     }
 
-    /**
-     * Checks [pipApp] window remains visible and on top throughout the transition
-     */
+    /** Checks [pipApp] window remains visible and on top throughout the transition */
     @Presubmit
     @Test
     fun pipAppWindowIsAlwaysOnTop() {
-        testSpec.assertWm {
-            isAppWindowOnTop(pipApp)
-        }
+        flicker.assertWm { isAppWindowOnTop(pipApp) }
     }
 
-    /**
-     * Checks that [testApp] window is not visible at the start
-     */
+    /** Checks that [testApp] window is not visible at the start */
     @Presubmit
     @Test
     fun testAppWindowInvisibleOnStart() {
-        testSpec.assertWmStart {
-            isAppWindowInvisible(testApp)
-        }
+        flicker.assertWmStart { isAppWindowInvisible(testApp) }
     }
 
-    /**
-     * Checks that [testApp] window is visible at the end
-     */
+    /** Checks that [testApp] window is visible at the end */
     @Presubmit
     @Test
     fun testAppWindowVisibleOnEnd() {
-        testSpec.assertWmEnd {
-            isAppWindowVisible(testApp)
-        }
+        flicker.assertWmEnd { isAppWindowVisible(testApp) }
     }
 
-    /**
-     * Checks that [testApp] layer is not visible at the start
-     */
+    /** Checks that [testApp] layer is not visible at the start */
     @Presubmit
     @Test
     fun testAppLayerInvisibleOnStart() {
-        testSpec.assertLayersStart {
-            isInvisible(testApp)
-        }
+        flicker.assertLayersStart { isInvisible(testApp) }
     }
 
-    /**
-     * Checks that [testApp] layer is visible at the end
-     */
+    /** Checks that [testApp] layer is visible at the end */
     @Presubmit
     @Test
     fun testAppLayerVisibleOnEnd() {
-        testSpec.assertLayersEnd {
-            isVisible(testApp)
-        }
+        flicker.assertLayersEnd { isVisible(testApp) }
     }
 
     /**
-     * Checks that the visible region of [pipApp] covers the full display area at the start of
-     * the transition
+     * Checks that the visible region of [pipApp] covers the full display area at the start of the
+     * transition
      */
     @Presubmit
     @Test
     fun pipAppLayerCoversFullScreenOnStart() {
-        testSpec.assertLayersStart {
-            visibleRegion(pipApp).coversExactly(startingBounds)
-        }
+        flicker.assertLayersStart { visibleRegion(pipApp).coversExactly(startingBounds) }
     }
 
     /**
-     * Checks that the visible region of [testApp] plus the visible region of [pipApp]
-     * cover the full display area at the end of the transition
+     * Checks that the visible region of [testApp] plus the visible region of [pipApp] cover the
+     * full display area at the end of the transition
      */
     @Presubmit
     @Test
     fun testAppPlusPipLayerCoversFullScreenOnEnd() {
-        testSpec.assertLayersEnd {
+        flicker.assertLayersEnd {
             val pipRegion = visibleRegion(pipApp).region
-            visibleRegion(testApp)
-                .plus(pipRegion)
-                .coversExactly(endingBounds)
+            visibleRegion(testApp).plus(pipRegion).coversExactly(endingBounds)
         }
     }
 
-    /** {@inheritDoc}  */
+    /** {@inheritDoc} */
     @Presubmit
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
@@ -239,16 +214,15 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
-         * repetitions, screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
index 1dc03b9..7466916 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
@@ -17,12 +17,12 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import org.junit.Test
 
 /** Base class for pip expand tests */
-abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+abstract class ExitPipToAppTransition(flicker: FlickerTest) : PipTransition(flicker) {
     protected val testApp = SimpleAppHelper(instrumentation)
 
     /**
@@ -32,7 +32,7 @@
     @Presubmit
     @Test
     open fun pipAppWindowRemainInsideVisibleBounds() {
-        testSpec.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+        flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
     /**
@@ -42,7 +42,7 @@
     @Presubmit
     @Test
     open fun pipAppLayerRemainInsideVisibleBounds() {
-        testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+        flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
     /**
@@ -52,7 +52,7 @@
     @Presubmit
     @Test
     open fun showBothAppWindowsThenHidePip() {
-        testSpec.assertWm {
+        flicker.assertWm {
             // when the activity is STOPPING, sometimes it becomes invisible in an entry before
             // the window, sometimes in the same entry. This occurs because we log 1x per frame
             // thus we ignore activity here
@@ -71,7 +71,7 @@
     @Presubmit
     @Test
     open fun showBothAppLayersThenHidePip() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             isVisible(testApp).isVisible(pipApp).then().isInvisible(testApp).isVisible(pipApp)
         }
     }
@@ -83,7 +83,7 @@
     @Presubmit
     @Test
     open fun testPlusPipAppsCoverFullScreenAtStart() {
-        testSpec.assertLayersStart {
+        flicker.assertLayersStart {
             val pipRegion = visibleRegion(pipApp).region
             visibleRegion(testApp).plus(pipRegion).coversExactly(displayBounds)
         }
@@ -96,14 +96,14 @@
     @Presubmit
     @Test
     open fun pipAppCoversFullScreenAtEnd() {
-        testSpec.assertLayersEnd { visibleRegion(pipApp).coversExactly(displayBounds) }
+        flicker.assertLayersEnd { visibleRegion(pipApp).coversExactly(displayBounds) }
     }
 
     /** Checks that the visible region of [pipApp] always expands during the animation */
     @Presubmit
     @Test
     open fun pipLayerExpands() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
             pipLayerList.zipWithNext { previous, current ->
                 current.visibleRegion.coversAtLeast(previous.visibleRegion.region)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
index 3b8bb90..1b5c227 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
@@ -17,20 +17,20 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.LAUNCHER
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Test
 
 /** Base class for exiting pip (closing pip window) without returning to the app */
-abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+abstract class ExitPipTransition(flicker: FlickerTest) : PipTransition(flicker) {
     override val transition: FlickerBuilder.() -> Unit
         get() = buildTransition {
-            setup { this.setRotation(testSpec.startRotation) }
-            teardown { this.setRotation(Surface.ROTATION_0) }
+            setup { this.setRotation(flicker.scenario.startRotation) }
+            teardown { this.setRotation(PlatformConsts.Rotation.ROTATION_0) }
         }
 
     /**
@@ -45,16 +45,16 @@
             // When Shell transition is enabled, we change the windowing mode at start, but
             // update the visibility after the transition is finished, so we can't check isNotPinned
             // and isAppWindowInvisible in the same assertion block.
-            testSpec.assertWm {
+            flicker.assertWm {
                 this.invoke("hasPipWindow") {
                         it.isPinned(pipApp).isAppWindowVisible(pipApp).isAppWindowOnTop(pipApp)
                     }
                     .then()
                     .invoke("!hasPipWindow") { it.isNotPinned(pipApp).isAppWindowNotOnTop(pipApp) }
             }
-            testSpec.assertWmEnd { isAppWindowInvisible(pipApp) }
+            flicker.assertWmEnd { isAppWindowInvisible(pipApp) }
         } else {
-            testSpec.assertWm {
+            flicker.assertWm {
                 this.invoke("hasPipWindow") { it.isPinned(pipApp).isAppWindowVisible(pipApp) }
                     .then()
                     .invoke("!hasPipWindow") { it.isNotPinned(pipApp).isAppWindowInvisible(pipApp) }
@@ -69,7 +69,7 @@
     @Presubmit
     @Test
     open fun pipLayerBecomesInvisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(pipApp)
                 .isVisible(LAUNCHER)
                 .then()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
index 6bf7e8c..1420f8ce 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
@@ -18,12 +18,12 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,30 +36,29 @@
  * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest`
  *
  * Actions:
+ * ```
  *     Launch an app in pip mode [pipApp],
  *     Launch another full screen mode [testApp]
  *     Expand [pipApp] app to full screen by clicking on the pip window and
  *     then on the expand button
- *
+ * ```
  * Notes:
+ * ```
  *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
  *        are inherited [PipTransition]
  *     2. Part of the test setup occurs automatically via
  *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
  *        including configuring navigation mode, initial orientation and ensuring no
  *        apps are running before setup
+ * ```
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExitPipViaExpandButtonClickTest(
-    testSpec: FlickerTestParameter
-) : ExitPipToAppTransition(testSpec) {
+class ExitPipViaExpandButtonClickTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) {
 
-    /**
-     * Defines the transition used to run the test
-     */
+    /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
         get() = buildTransition {
             setup {
@@ -70,34 +69,29 @@
                 // This will bring PipApp to fullscreen
                 pipApp.expandPipWindowToApp(wmHelper)
                 // Wait until the other app is no longer visible
-                wmHelper.StateSyncBuilder()
-                    .withWindowSurfaceDisappeared(testApp)
-                    .waitForAndVerify()
+                wmHelper.StateSyncBuilder().withWindowSurfaceDisappeared(testApp).waitForAndVerify()
             }
         }
 
-    /** {@inheritDoc}  */
-    @Presubmit
-    @Test
-    override fun entireScreenCovered() = super.entireScreenCovered()
+    /** {@inheritDoc} */
+    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
-    /** {@inheritDoc}  */
-    @FlakyTest(bugId = 197726610)
-    @Test
-    override fun pipLayerExpands() = super.pipLayerExpands()
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 197726610) @Test override fun pipLayerExpands() = super.pipLayerExpands()
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
-         * repetitions, screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
index 3356d3e..dffbe7e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
@@ -18,13 +18,13 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -57,7 +57,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransition(testSpec) {
+class ExitPipViaIntentTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) {
 
     /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
@@ -74,10 +74,8 @@
             }
         }
 
-    /** {@inheritDoc}  */
-    @Presubmit
-    @Test
-    override fun entireScreenCovered() = super.entireScreenCovered()
+    /** {@inheritDoc} */
+    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Presubmit
@@ -113,14 +111,15 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+         * and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
index d195abb..232c025 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
@@ -17,12 +17,12 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,7 +54,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) {
+class ExitPipWithDismissButtonTest(flicker: FlickerTest) : ExitPipTransition(flicker) {
 
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -69,21 +69,22 @@
     @Presubmit
     @Test
     fun focusChanges() {
-        testSpec.assertEventLog { this.focusChanges("PipMenuView", "NexusLauncherActivity") }
+        flicker.assertEventLog { this.focusChanges("PipMenuView", "NexusLauncherActivity") }
     }
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+         * and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
index f7a2447..dbbfdcc 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
@@ -17,13 +17,13 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,7 +54,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) {
+class ExitPipWithSwipeDownTest(flicker: FlickerTest) : ExitPipTransition(flicker) {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
@@ -64,7 +64,7 @@
                 val pipCenterY = pipRegion.centerY()
                 val displayCenterX = device.displayWidth / 2
                 val barComponent =
-                    if (testSpec.isTablet) {
+                    if (flicker.scenario.isTablet) {
                         ComponentNameMatcher.TASK_BAR
                     } else {
                         ComponentNameMatcher.NAV_BAR
@@ -92,21 +92,22 @@
     @Presubmit
     @Test
     fun focusDoesNotChange() {
-        testSpec.assertEventLog { this.focusDoesNotChange() }
+        flicker.assertEventLog { this.focusDoesNotChange() }
     }
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+         * and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
index fa5ce5b..f213cc9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
@@ -17,13 +17,13 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.FlakyTest
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,28 +36,27 @@
  * To run this test: `atest WMShellFlickerTests:ExpandPipOnDoubleClickTest`
  *
  * Actions:
+ * ```
  *     Launch an app in pip mode [pipApp],
  *     Expand [pipApp] app to its maximum pip size by double clicking on it
- *
+ * ```
  * Notes:
+ * ```
  *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
  *        are inherited [PipTransition]
  *     2. Part of the test setup occurs automatically via
  *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
  *        including configuring navigation mode, initial orientation and ensuring no
  *        apps are running before setup
+ * ```
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) {
     override val transition: FlickerBuilder.() -> Unit
-        get() = buildTransition {
-            transitions {
-                pipApp.doubleClickPipWindow(wmHelper)
-            }
-        }
+        get() = buildTransition { transitions { pipApp.doubleClickPipWindow(wmHelper) } }
 
     /**
      * Checks that the pip app window remains inside the display bounds throughout the whole
@@ -66,9 +65,7 @@
     @FlakyTest(bugId = 249308003)
     @Test
     fun pipWindowRemainInsideVisibleBounds() {
-        testSpec.assertWmVisibleRegion(pipApp) {
-            coversAtMost(displayBounds)
-        }
+        flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
     /**
@@ -78,40 +75,28 @@
     @FlakyTest(bugId = 249308003)
     @Test
     fun pipLayerRemainInsideVisibleBounds() {
-        testSpec.assertLayersVisibleRegion(pipApp) {
-            coversAtMost(displayBounds)
-        }
+        flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
-    /**
-     * Checks [pipApp] window remains visible throughout the animation
-     */
+    /** Checks [pipApp] window remains visible throughout the animation */
     @FlakyTest(bugId = 249308003)
     @Test
     fun pipWindowIsAlwaysVisible() {
-        testSpec.assertWm {
-            isAppWindowVisible(pipApp)
-        }
+        flicker.assertWm { isAppWindowVisible(pipApp) }
     }
 
-    /**
-     * Checks [pipApp] layer remains visible throughout the animation
-     */
+    /** Checks [pipApp] layer remains visible throughout the animation */
     @FlakyTest(bugId = 249308003)
     @Test
     fun pipLayerIsAlwaysVisible() {
-        testSpec.assertLayers {
-            isVisible(pipApp)
-        }
+        flicker.assertLayers { isVisible(pipApp) }
     }
 
-    /**
-     * Checks that the visible region of [pipApp] always expands during the animation
-     */
+    /** Checks that the visible region of [pipApp] always expands during the animation */
     @FlakyTest(bugId = 249308003)
     @Test
     fun pipLayerExpands() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
             pipLayerList.zipWithNext { previous, current ->
                 current.visibleRegion.coversAtLeast(previous.visibleRegion.region)
@@ -122,7 +107,7 @@
     @FlakyTest(bugId = 249308003)
     @Test
     fun pipSameAspectRatio() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
             pipLayerList.zipWithNext { previous, current ->
                 current.visibleRegion.isSameAspectRatio(previous.visibleRegion)
@@ -130,37 +115,25 @@
         }
     }
 
-    /**
-     * Checks [pipApp] window remains pinned throughout the animation
-     */
+    /** Checks [pipApp] window remains pinned throughout the animation */
     @FlakyTest(bugId = 249308003)
     @Test
     fun windowIsAlwaysPinned() {
-        testSpec.assertWm {
-            this.invoke("hasPipWindow") { it.isPinned(pipApp) }
-        }
+        flicker.assertWm { this.invoke("hasPipWindow") { it.isPinned(pipApp) } }
     }
 
-    /**
-     * Checks [ComponentMatcher.LAUNCHER] layer remains visible throughout the animation
-     */
+    /** Checks [ComponentMatcher.LAUNCHER] layer remains visible throughout the animation */
     @FlakyTest(bugId = 249308003)
     @Test
     fun launcherIsAlwaysVisible() {
-        testSpec.assertLayers {
-            isVisible(ComponentNameMatcher.LAUNCHER)
-        }
+        flicker.assertLayers { isVisible(ComponentNameMatcher.LAUNCHER) }
     }
 
-    /**
-     * Checks that the focus doesn't change between windows during the transition
-     */
+    /** Checks that the focus doesn't change between windows during the transition */
     @FlakyTest(bugId = 216306753)
     @Test
     fun focusDoesNotChange() {
-        testSpec.assertEventLog {
-            this.focusDoesNotChange()
-        }
+        flicker.assertEventLog { this.focusDoesNotChange() }
     }
 
     @FlakyTest(bugId = 216306753)
@@ -233,16 +206,15 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
-         * repetitions, screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0)
-                )
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
index bcd01a4..34f6659 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
@@ -17,40 +17,32 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Postsubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
 
-/**
- * Test expanding a pip window via pinch out gesture.
- */
+/** Test expanding a pip window via pinch out gesture. */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExpandPipOnPinchOpenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+class ExpandPipOnPinchOpenTest(flicker: FlickerTest) : PipTransition(flicker) {
     override val transition: FlickerBuilder.() -> Unit
-        get() = buildTransition {
-            transitions {
-                pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30)
-            }
-        }
+        get() = buildTransition { transitions { pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30) } }
 
-    /**
-     * Checks that the visible region area of [pipApp] always increases during the animation.
-     */
+    /** Checks that the visible region area of [pipApp] always increases during the animation. */
     @Postsubmit
     @Test
     fun pipLayerAreaIncreases() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
             pipLayerList.zipWithNext { previous, current ->
                 previous.visibleRegion.notBiggerThan(current.visibleRegion.region)
@@ -62,16 +54,15 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
-         * repetitions, screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0)
-                )
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt
index 0c0228e..e9847fa 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt
@@ -18,11 +18,11 @@
 
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.Direction
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -36,71 +36,56 @@
  * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest`
  *
  * Actions:
+ * ```
  *     Launch [pipApp] in pip mode
  *     Press home
  *     Launch [testApp]
  *     Check if pip window moves down (visually)
- *
+ * ```
  * Notes:
+ * ```
  *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
  *        are inherited [PipTransition]
  *     2. Part of the test setup occurs automatically via
  *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
  *        including configuring navigation mode, initial orientation and ensuring no
  *        apps are running before setup
+ * ```
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class MovePipDownShelfHeightChangeTest(
-    testSpec: FlickerTestParameter
-) : MovePipShelfHeightTransition(testSpec) {
-//    @Before
-//    fun before() {
-//        Assume.assumeFalse(isShellTransitionsEnabled)
-//    }
-
-    /**
-     * Defines the transition used to run the test
-     */
+class MovePipDownShelfHeightChangeTest(flicker: FlickerTest) :
+    MovePipShelfHeightTransition(flicker) {
+    /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
         get() = buildTransition {
             teardown {
                 tapl.pressHome()
                 testApp.exit(wmHelper)
             }
-            transitions {
-                testApp.launchViaIntent(wmHelper)
-            }
+            transitions { testApp.launchViaIntent(wmHelper) }
         }
 
-    /**
-     * Checks that the visible region of [pipApp] window always moves down during the animation.
-     */
-    @Presubmit
-    @Test
-    fun pipWindowMovesDown() = pipWindowMoves(Direction.DOWN)
+    /** Checks that the visible region of [pipApp] window always moves down during the animation. */
+    @Presubmit @Test fun pipWindowMovesDown() = pipWindowMoves(Direction.DOWN)
 
-    /**
-     * Checks that the visible region of [pipApp] layer always moves down during the animation.
-     */
-    @Presubmit
-    @Test
-    fun pipLayerMovesDown() = pipLayerMoves(Direction.DOWN)
+    /** Checks that the visible region of [pipApp] layer always moves down during the animation. */
+    @Presubmit @Test fun pipLayerMovesDown() = pipLayerMoves(Direction.DOWN)
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
-         * repetitions, screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
-                supportedRotations = listOf(Surface.ROTATION_0)
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
             )
         }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
index b401067..35525cb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
@@ -17,29 +17,28 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
 import com.android.server.wm.flicker.traces.region.RegionSubject
 import com.android.wm.shell.flicker.Direction
 import org.junit.Test
 
 /** Base class for pip tests with Launcher shelf height change */
-abstract class MovePipShelfHeightTransition(testSpec: FlickerTestParameter) :
-    PipTransition(testSpec) {
+abstract class MovePipShelfHeightTransition(flicker: FlickerTest) : PipTransition(flicker) {
     protected val testApp = FixedOrientationAppHelper(instrumentation)
 
     /** Checks [pipApp] window remains visible throughout the animation */
     @Presubmit
     @Test
     open fun pipWindowIsAlwaysVisible() {
-        testSpec.assertWm { isAppWindowVisible(pipApp) }
+        flicker.assertWm { isAppWindowVisible(pipApp) }
     }
 
     /** Checks [pipApp] layer remains visible throughout the animation */
     @Presubmit
     @Test
     open fun pipLayerIsAlwaysVisible() {
-        testSpec.assertLayers { isVisible(pipApp) }
+        flicker.assertLayers { isVisible(pipApp) }
     }
 
     /**
@@ -49,7 +48,7 @@
     @Presubmit
     @Test
     open fun pipWindowRemainInsideVisibleBounds() {
-        testSpec.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+        flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
     /**
@@ -59,7 +58,7 @@
     @Presubmit
     @Test
     open fun pipLayerRemainInsideVisibleBounds() {
-        testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
+        flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
     /**
@@ -67,7 +66,7 @@
      * during the animation.
      */
     protected fun pipWindowMoves(direction: Direction) {
-        testSpec.assertWm {
+        flicker.assertWm {
             val pipWindowFrameList =
                 this.windowStates { pipApp.windowMatchesAnyOf(it) && it.isVisible }.map { it.frame }
             when (direction) {
@@ -83,7 +82,7 @@
      * during the animation.
      */
     protected fun pipLayerMoves(direction: Direction) {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             val pipLayerRegionList =
                 this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
                     .map { it.visibleRegion }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt
index 7f8ef32..3a12a34 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt
@@ -17,12 +17,12 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.Direction
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -36,68 +36,55 @@
  * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest`
  *
  * Actions:
+ * ```
  *     Launch [pipApp] in pip mode
  *     Launch [testApp]
  *     Press home
  *     Check if pip window moves up (visually)
- *
+ * ```
  * Notes:
+ * ```
  *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
  *        are inherited [PipTransition]
  *     2. Part of the test setup occurs automatically via
  *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
  *        including configuring navigation mode, initial orientation and ensuring no
  *        apps are running before setup
+ * ```
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class MovePipUpShelfHeightChangeTest(
-    testSpec: FlickerTestParameter
-) : MovePipShelfHeightTransition(testSpec) {
-    /**
-     * Defines the transition used to run the test
-     */
+open class MovePipUpShelfHeightChangeTest(flicker: FlickerTest) :
+    MovePipShelfHeightTransition(flicker) {
+    /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
-        get() = buildTransition() {
-            setup {
-                testApp.launchViaIntent(wmHelper)
+        get() =
+            buildTransition() {
+                setup { testApp.launchViaIntent(wmHelper) }
+                transitions { tapl.pressHome() }
+                teardown { testApp.exit(wmHelper) }
             }
-            transitions {
-                tapl.pressHome()
-            }
-            teardown {
-                testApp.exit(wmHelper)
-            }
-        }
 
-    /**
-     * Checks that the visible region of [pipApp] window always moves up during the animation.
-     */
-    @Presubmit
-    @Test
-    fun pipWindowMovesUp() = pipWindowMoves(Direction.UP)
+    /** Checks that the visible region of [pipApp] window always moves up during the animation. */
+    @Presubmit @Test fun pipWindowMovesUp() = pipWindowMoves(Direction.UP)
 
-    /**
-     * Checks that the visible region of [pipApp] layer always moves up during the animation.
-     */
-    @Presubmit
-    @Test
-    fun pipLayerMovesUp() = pipLayerMoves(Direction.UP)
+    /** Checks that the visible region of [pipApp] layer always moves up during the animation. */
+    @Presubmit @Test fun pipLayerMovesUp() = pipLayerMoves(Direction.UP)
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
-         * repetitions, screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
-                supportedRotations = listOf(Surface.ROTATION_0)
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
             )
         }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
index 3b64d21..12d6362 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt
@@ -17,17 +17,17 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -41,7 +41,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+open class PipKeyboardTest(flicker: FlickerTest) : PipTransition(flicker) {
     private val imeApp = ImeAppHelper(instrumentation)
 
     @Before
@@ -54,11 +54,11 @@
         get() = buildTransition {
             setup {
                 imeApp.launchViaIntent(wmHelper)
-                setRotation(testSpec.startRotation)
+                setRotation(flicker.scenario.startRotation)
             }
             teardown {
                 imeApp.exit(wmHelper)
-                setRotation(Surface.ROTATION_0)
+                setRotation(PlatformConsts.Rotation.ROTATION_0)
             }
             transitions {
                 // open the soft keyboard
@@ -74,8 +74,8 @@
     @Presubmit
     @Test
     open fun pipInVisibleBounds() {
-        testSpec.assertWmVisibleRegion(pipApp) {
-            val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
+        flicker.assertWmVisibleRegion(pipApp) {
+            val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
             coversAtMost(displayBounds)
         }
     }
@@ -84,7 +84,7 @@
     @Presubmit
     @Test
     open fun pipIsAboveAppWindow() {
-        testSpec.assertWmTag(TAG_IME_VISIBLE) { isAboveWindow(ComponentNameMatcher.IME, pipApp) }
+        flicker.assertWmTag(TAG_IME_VISIBLE) { isAboveWindow(ComponentNameMatcher.IME, pipApp) }
     }
 
     companion object {
@@ -92,9 +92,10 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt
index 2a82c00..901814e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTestShellTransit.kt
@@ -18,9 +18,9 @@
 
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -33,7 +33,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class PipKeyboardTestShellTransit(testSpec: FlickerTestParameter) : PipKeyboardTest(testSpec) {
+class PipKeyboardTestShellTransit(flicker: FlickerTest) : PipKeyboardTest(flicker) {
 
     @Before
     override fun before() {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
index 7de5494..eee00bd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
@@ -18,16 +18,15 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -45,7 +44,7 @@
  * ```
  *     Launch a [pipApp] in pip mode
  *     Launch another app [fixedApp] (appears below pip)
- *     Rotate the screen from [testSpec.startRotation] to [testSpec.endRotation]
+ *     Rotate the screen from [flicker.scenario.startRotation] to [flicker.scenario.endRotation]
  *     (usually, 0->90 and 90->0)
  * ```
  * Notes:
@@ -62,10 +61,10 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+open class PipRotationTest(flicker: FlickerTest) : PipTransition(flicker) {
     private val testApp = SimpleAppHelper(instrumentation)
-    private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.startRotation)
-    private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.endRotation)
+    private val screenBoundsStart = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
+    private val screenBoundsEnd = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
 
     @Before
     open fun before() {
@@ -76,9 +75,9 @@
         get() = buildTransition {
             setup {
                 testApp.launchViaIntent(wmHelper)
-                setRotation(testSpec.startRotation)
+                setRotation(flicker.scenario.startRotation)
             }
-            transitions { setRotation(testSpec.endRotation) }
+            transitions { setRotation(flicker.scenario.endRotation) }
         }
 
     /** Checks the position of the navigation bar at the start and end of the transition */
@@ -90,14 +89,14 @@
     @Presubmit
     @Test
     fun fixedAppLayer_StartingBounds() {
-        testSpec.assertLayersStart { visibleRegion(testApp).coversAtMost(screenBoundsStart) }
+        flicker.assertLayersStart { visibleRegion(testApp).coversAtMost(screenBoundsStart) }
     }
 
     /** Checks that [testApp] layer is within [screenBoundsEnd] at the end of the transition */
     @Presubmit
     @Test
     fun fixedAppLayer_EndingBounds() {
-        testSpec.assertLayersEnd { visibleRegion(testApp).coversAtMost(screenBoundsEnd) }
+        flicker.assertLayersEnd { visibleRegion(testApp).coversAtMost(screenBoundsEnd) }
     }
 
     /**
@@ -107,7 +106,7 @@
     @Presubmit
     @Test
     fun appLayers_StartingBounds() {
-        testSpec.assertLayersStart {
+        flicker.assertLayersStart {
             visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsStart)
         }
     }
@@ -119,14 +118,12 @@
     @Presubmit
     @Test
     fun appLayers_EndingBounds() {
-        testSpec.assertLayersEnd {
-            visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsEnd)
-        }
+        flicker.assertLayersEnd { visibleRegion(testApp.or(pipApp)).coversExactly(screenBoundsEnd) }
     }
 
     /** Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition */
     private fun pipLayerRotates_StartingBounds_internal() {
-        testSpec.assertLayersStart { visibleRegion(pipApp).coversAtMost(screenBoundsStart) }
+        flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(screenBoundsStart) }
     }
 
     /** Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition */
@@ -140,7 +137,7 @@
     @Presubmit
     @Test
     fun pipLayerRotates_EndingBounds() {
-        testSpec.assertLayersEnd { visibleRegion(pipApp).coversAtMost(screenBoundsEnd) }
+        flicker.assertLayersEnd { visibleRegion(pipApp).coversAtMost(screenBoundsEnd) }
     }
 
     /**
@@ -149,7 +146,7 @@
     @Presubmit
     @Test
     fun pipIsAboveFixedAppWindow_Start() {
-        testSpec.assertWmStart { isAboveWindow(pipApp, testApp) }
+        flicker.assertWmStart { isAboveWindow(pipApp, testApp) }
     }
 
     /**
@@ -158,7 +155,7 @@
     @Presubmit
     @Test
     fun pipIsAboveFixedAppWindow_End() {
-        testSpec.assertWmEnd { isAboveWindow(pipApp, testApp) }
+        flicker.assertWmEnd { isAboveWindow(pipApp, testApp) }
     }
 
     @Presubmit
@@ -171,16 +168,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
+         * and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.rotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt
index 983cb1c..d0d9167 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt
@@ -18,9 +18,9 @@
 
 import android.platform.test.annotations.FlakyTest
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -38,7 +38,7 @@
  * ```
  *     Launch a [pipApp] in pip mode
  *     Launch another app [fixedApp] (appears below pip)
- *     Rotate the screen from [testSpec.startRotation] to [testSpec.endRotation]
+ *     Rotate the screen from [flicker.scenario.startRotation] to [flicker.scenario.endRotation]
  *     (usually, 0->90 and 90->0)
  * ```
  * Notes:
@@ -56,7 +56,7 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @FlakyTest(bugId = 239575053)
-class PipRotationTest_ShellTransit(testSpec: FlickerTestParameter) : PipRotationTest(testSpec) {
+class PipRotationTest_ShellTransit(flicker: FlickerTest) : PipRotationTest(flicker) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
index dfa2510..0e0be79 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
@@ -18,19 +18,19 @@
 
 import android.app.Instrumentation
 import android.content.Intent
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.PipAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
 import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.BaseTest
 
-abstract class PipTransition(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) {
     protected val pipApp = PipAppHelper(instrumentation)
-    protected val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
+    protected val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
     protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation)
 
     // Helper class to process test actions by broadcast.
@@ -67,12 +67,12 @@
     ): FlickerBuilder.() -> Unit {
         return {
             setup {
-                setRotation(Surface.ROTATION_0)
+                setRotation(PlatformConsts.Rotation.ROTATION_0)
                 removeAllTasksButHome()
                 pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras)
             }
             teardown {
-                setRotation(Surface.ROTATION_0)
+                setRotation(PlatformConsts.Rotation.ROTATION_0)
                 removeAllTasksButHome()
                 pipApp.exit(wmHelper)
             }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
index f0093e6..157aa98 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
@@ -20,19 +20,19 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
 import org.junit.Assume
 import org.junit.Before
@@ -43,20 +43,18 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test exiting Pip with orientation changes.
- * To run this test: `atest WMShellFlickerTests:SetRequestedOrientationWhilePinnedTest`
+ * Test exiting Pip with orientation changes. To run this test: `atest
+ * WMShellFlickerTests:SetRequestedOrientationWhilePinnedTest`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SetRequestedOrientationWhilePinnedTest(
-    testSpec: FlickerTestParameter
-) : PipTransition(testSpec) {
-    private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0)
-    private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90)
+open class SetRequestedOrientationWhilePinnedTest(flicker: FlickerTest) : PipTransition(flicker) {
+    private val startingBounds = WindowUtils.getDisplayBounds(PlatformConsts.Rotation.ROTATION_0)
+    private val endingBounds = WindowUtils.getDisplayBounds(PlatformConsts.Rotation.ROTATION_90)
 
-    /** {@inheritDoc}  */
+    /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             setup {
@@ -64,30 +62,35 @@
                 device.wakeUpAndGoToHomeScreen()
 
                 // Launch the PiP activity fixed as landscape.
-                pipApp.launchViaIntent(wmHelper, stringExtras = mapOf(
-                    EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString()))
+                pipApp.launchViaIntent(
+                    wmHelper,
+                    stringExtras =
+                        mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())
+                )
                 // Enter PiP.
                 broadcastActionTrigger.doAction(ActivityOptions.Pip.ACTION_ENTER_PIP)
                 // System bar may fade out during fixed rotation.
-                wmHelper.StateSyncBuilder()
+                wmHelper
+                    .StateSyncBuilder()
                     .withPipShown()
-                    .withRotation(Surface.ROTATION_0)
+                    .withRotation(PlatformConsts.Rotation.ROTATION_0)
                     .withNavOrTaskBarVisible()
                     .withStatusBarVisible()
                     .waitForAndVerify()
             }
             teardown {
                 pipApp.exit(wmHelper)
-                setRotation(Surface.ROTATION_0)
+                setRotation(PlatformConsts.Rotation.ROTATION_0)
                 removeAllTasksButHome()
             }
             transitions {
                 // Launch the activity back into fullscreen and ensure that it is now in landscape
                 pipApp.launchViaIntent(wmHelper)
                 // System bar may fade out during fixed rotation.
-                wmHelper.StateSyncBuilder()
+                wmHelper
+                    .StateSyncBuilder()
                     .withFullScreenApp(pipApp)
-                    .withRotation(Surface.ROTATION_90)
+                    .withRotation(PlatformConsts.Rotation.ROTATION_90)
                     .withNavOrTaskBarVisible()
                     .withStatusBarVisible()
                     .waitForAndVerify()
@@ -95,34 +98,32 @@
         }
 
     /**
-     * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation]
-     * to fix a orientation, Tablets instead keep the same orientation and add letterboxes
+     * This test is not compatible with Tablets. When using [Activity.setRequestedOrientation] to
+     * fix a orientation, Tablets instead keep the same orientation and add letterboxes
      */
     @Before
     fun setup() {
-        Assume.assumeFalse(testSpec.isTablet)
+        Assume.assumeFalse(flicker.scenario.isTablet)
     }
 
     @Presubmit
     @Test
     fun displayEndsAt90Degrees() {
-        testSpec.assertWmEnd {
-            hasRotation(Surface.ROTATION_90)
-        }
+        flicker.assertWmEnd { hasRotation(PlatformConsts.Rotation.ROTATION_90) }
     }
 
-    /** {@inheritDoc}  */
+    /** {@inheritDoc} */
     @Presubmit
     @Test
     override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
-    /** {@inheritDoc}  */
+    /** {@inheritDoc} */
     @Presubmit
     @Test
     override fun statusBarLayerIsVisibleAtStartAndEnd() =
         super.statusBarLayerIsVisibleAtStartAndEnd()
 
-    /** {@inheritDoc}  */
+    /** {@inheritDoc} */
     @FlakyTest
     @Test
     override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
@@ -130,23 +131,17 @@
     @Presubmit
     @Test
     fun pipWindowInsideDisplay() {
-        testSpec.assertWmStart {
-            visibleRegion(pipApp).coversAtMost(startingBounds)
-        }
+        flicker.assertWmStart { visibleRegion(pipApp).coversAtMost(startingBounds) }
     }
 
     @Presubmit
     @Test
     fun pipAppShowsOnTop() {
-        testSpec.assertWmEnd {
-            isAppWindowOnTop(pipApp)
-        }
+        flicker.assertWmEnd { isAppWindowOnTop(pipApp) }
     }
 
     private fun pipLayerInsideDisplay_internal() {
-        testSpec.assertLayersStart {
-            visibleRegion(pipApp).coversAtMost(startingBounds)
-        }
+        flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(startingBounds) }
     }
 
     @Presubmit
@@ -166,40 +161,35 @@
     @Presubmit
     @Test
     fun pipAlwaysVisible() {
-        testSpec.assertWm {
-            this.isAppWindowVisible(pipApp)
-        }
+        flicker.assertWm { this.isAppWindowVisible(pipApp) }
     }
 
     @Presubmit
     @Test
     fun pipAppLayerCoversFullScreen() {
-        testSpec.assertLayersEnd {
-            visibleRegion(pipApp).coversExactly(endingBounds)
-        }
+        flicker.assertLayersEnd { visibleRegion(pipApp).coversExactly(endingBounds) }
     }
 
-    /** {@inheritDoc}  */
+    /** {@inheritDoc} */
     @Postsubmit
     @Test
     override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
-    /** {@inheritDoc}  */
+    /** {@inheritDoc} */
     @Postsubmit
     @Test
     override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
-    /** {@inheritDoc}  */
-    @Postsubmit
-    @Test
-    override fun entireScreenCovered() = super.entireScreenCovered()
+    /** {@inheritDoc} */
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt
index 2cb18f9..a16f5f6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt
@@ -24,7 +24,10 @@
 import org.junit.Before
 import org.junit.runners.Parameterized
 
-abstract class PipTestBase(protected val rotationName: String, protected val rotation: Int) {
+abstract class PipTestBase(
+    protected val rotationName: String,
+    protected val rotation: Int
+) {
     val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     val uiDevice = UiDevice.getInstance(instrumentation)
     val packageManager: PackageManager = instrumentation.context.packageManager
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 9533b91..65cbea0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -20,10 +20,10 @@
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.EdgeExtensionComponentMatcher
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
@@ -49,7 +49,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class CopyContentInSplit(flicker: FlickerTest) : SplitScreenBase(flicker) {
     private val textEditApp = SplitScreenUtils.getIme(instrumentation)
     private val MagnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#")
     private val PopupWindowLayer = ComponentNameMatcher("", "PopupWindow:")
@@ -72,29 +72,29 @@
     @Presubmit
     @Test
     fun cujCompleted() {
-        testSpec.appWindowIsVisibleAtStart(primaryApp)
-        testSpec.appWindowIsVisibleAtStart(textEditApp)
-        testSpec.splitScreenDividerIsVisibleAtStart()
+        flicker.appWindowIsVisibleAtStart(primaryApp)
+        flicker.appWindowIsVisibleAtStart(textEditApp)
+        flicker.splitScreenDividerIsVisibleAtStart()
 
-        testSpec.appWindowIsVisibleAtEnd(primaryApp)
-        testSpec.appWindowIsVisibleAtEnd(textEditApp)
-        testSpec.splitScreenDividerIsVisibleAtEnd()
+        flicker.appWindowIsVisibleAtEnd(primaryApp)
+        flicker.appWindowIsVisibleAtEnd(textEditApp)
+        flicker.splitScreenDividerIsVisibleAtEnd()
 
         // The validation of copied text is already done in SplitScreenUtils.copyContentInSplit()
     }
 
     @Presubmit
     @Test
-    fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+    fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
 
-    @Presubmit @Test fun primaryAppLayerKeepVisible() = testSpec.layerKeepVisible(primaryApp)
+    @Presubmit @Test fun primaryAppLayerKeepVisible() = flicker.layerKeepVisible(primaryApp)
 
-    @Presubmit @Test fun textEditAppLayerKeepVisible() = testSpec.layerKeepVisible(textEditApp)
+    @Presubmit @Test fun textEditAppLayerKeepVisible() = flicker.layerKeepVisible(textEditApp)
 
     @Presubmit
     @Test
     fun primaryAppBoundsKeepVisible() =
-        testSpec.splitAppLayerBoundsKeepVisible(
+        flicker.splitAppLayerBoundsKeepVisible(
             primaryApp,
             landscapePosLeft = tapl.isTablet,
             portraitPosTop = false
@@ -103,21 +103,18 @@
     @Presubmit
     @Test
     fun textEditAppBoundsKeepVisible() =
-        testSpec.splitAppLayerBoundsKeepVisible(
+        flicker.splitAppLayerBoundsKeepVisible(
             textEditApp,
             landscapePosLeft = !tapl.isTablet,
             portraitPosTop = true
         )
 
-    @Presubmit @Test fun primaryAppWindowKeepVisible() = testSpec.appWindowKeepVisible(primaryApp)
+    @Presubmit @Test fun primaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(primaryApp)
 
-    @Presubmit @Test fun textEditAppWindowKeepVisible() = testSpec.appWindowKeepVisible(textEditApp)
+    @Presubmit @Test fun textEditAppWindowKeepVisible() = flicker.appWindowKeepVisible(textEditApp)
 
     /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Presubmit
@@ -164,15 +161,18 @@
     @Presubmit
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.visibleLayersShownMoreThanOneConsecutiveEntry(
-                ignoreLayers = listOf(
-                    ComponentNameMatcher.SPLASH_SCREEN,
-                    ComponentNameMatcher.SNAPSHOT,
-                    ComponentNameMatcher.IME_SNAPSHOT,
-                    EdgeExtensionComponentMatcher(),
-                    MagnifierLayer,
-                    PopupWindowLayer))
+                ignoreLayers =
+                    listOf(
+                        ComponentNameMatcher.SPLASH_SCREEN,
+                        ComponentNameMatcher.SNAPSHOT,
+                        ComponentNameMatcher.IME_SNAPSHOT,
+                        EdgeExtensionComponentMatcher(),
+                        MagnifierLayer,
+                        PopupWindowLayer
+                    )
+            )
         }
     }
 
@@ -185,9 +185,8 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests()
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
index 4757498..d0f02e2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
@@ -21,11 +21,11 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowBecomesInvisible
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -49,54 +49,62 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class DismissSplitScreenByDivider(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
-            setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
-            }
+            setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
             transitions {
                 if (tapl.isTablet) {
-                    SplitScreenUtils.dragDividerToDismissSplit(device, wmHelper,
-                        dragToRight = false, dragToBottom = true)
+                    SplitScreenUtils.dragDividerToDismissSplit(
+                        device,
+                        wmHelper,
+                        dragToRight = false,
+                        dragToBottom = true
+                    )
                 } else {
-                    SplitScreenUtils.dragDividerToDismissSplit(device, wmHelper,
-                        dragToRight = true, dragToBottom = true)
+                    SplitScreenUtils.dragDividerToDismissSplit(
+                        device,
+                        wmHelper,
+                        dragToRight = true,
+                        dragToBottom = true
+                    )
                 }
-                wmHelper.StateSyncBuilder()
-                    .withFullScreenApp(secondaryApp)
-                    .waitForAndVerify()
+                wmHelper.StateSyncBuilder().withFullScreenApp(secondaryApp).waitForAndVerify()
             }
         }
 
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenDismissed(primaryApp, secondaryApp, toHome = false)
+    fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = false)
 
     @Presubmit
     @Test
-    fun splitScreenDividerBecomesInvisible() = testSpec.splitScreenDividerBecomesInvisible()
+    fun splitScreenDividerBecomesInvisible() = flicker.splitScreenDividerBecomesInvisible()
 
     @Presubmit
     @Test
-    fun primaryAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(primaryApp)
+    fun primaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp)
+    fun secondaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(secondaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
-        primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false)
+    fun primaryAppBoundsBecomesInvisible() =
+        flicker.splitAppLayerBoundsBecomesInvisible(
+            primaryApp,
+            landscapePosLeft = tapl.isTablet,
+            portraitPosTop = false
+        )
 
     @Presubmit
     @Test
     fun secondaryAppBoundsIsFullscreenAtEnd() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(secondaryApp)
                 .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
                 .then()
@@ -109,7 +117,7 @@
                 .contains(SPLIT_SCREEN_DIVIDER_COMPONENT)
                 .then()
                 .invoke("secondaryAppBoundsIsFullscreenAtEnd") {
-                    val displayBounds = WindowUtils.getDisplayBounds(testSpec.endRotation)
+                    val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
                     it.visibleRegion(secondaryApp).coversExactly(displayBounds)
                 }
         }
@@ -117,35 +125,29 @@
 
     @Presubmit
     @Test
-    fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp)
+    fun primaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp)
+    fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp)
+
+    /** {@inheritDoc} */
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 206753786)
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -156,26 +158,22 @@
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -192,8 +190,8 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
index 1d61955..b44b681 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
@@ -20,10 +20,10 @@
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.wm.shell.flicker.appWindowBecomesInvisible
 import com.android.wm.shell.flicker.layerBecomesInvisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
@@ -44,89 +44,81 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class DismissSplitScreenByGoHome(
-    testSpec: FlickerTestParameter
-) : SplitScreenBase(testSpec) {
+class DismissSplitScreenByGoHome(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
-            setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
-            }
+            setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
             transitions {
                 tapl.goHome()
-                wmHelper.StateSyncBuilder()
-                    .withHomeActivityVisible()
-                    .waitForAndVerify()
+                wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
             }
         }
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenDismissed(primaryApp, secondaryApp, toHome = true)
+    fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = true)
 
     @Presubmit
     @Test
-    fun splitScreenDividerBecomesInvisible() = testSpec.splitScreenDividerBecomesInvisible()
+    fun splitScreenDividerBecomesInvisible() = flicker.splitScreenDividerBecomesInvisible()
 
     @FlakyTest(bugId = 241525302)
     @Test
-    fun primaryAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(primaryApp)
+    fun primaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(primaryApp)
 
     // TODO(b/245472831): Move back to presubmit after shell transitions landing.
     @FlakyTest(bugId = 245472831)
     @Test
-    fun secondaryAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(primaryApp)
+    fun secondaryAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(primaryApp)
 
     // TODO(b/245472831): Move back to presubmit after shell transitions landing.
     @FlakyTest(bugId = 245472831)
     @Test
-    fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
-        primaryApp,
-        landscapePosLeft = tapl.isTablet,
-        portraitPosTop = false
-    )
+    fun primaryAppBoundsBecomesInvisible() =
+        flicker.splitAppLayerBoundsBecomesInvisible(
+            primaryApp,
+            landscapePosLeft = tapl.isTablet,
+            portraitPosTop = false
+        )
 
     @FlakyTest(bugId = 250530241)
     @Test
-    fun secondaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
-        secondaryApp,
-        landscapePosLeft = !tapl.isTablet,
-        portraitPosTop = true
-    )
+    fun secondaryAppBoundsBecomesInvisible() =
+        flicker.splitAppLayerBoundsBecomesInvisible(
+            secondaryApp,
+            landscapePosLeft = !tapl.isTablet,
+            portraitPosTop = true
+        )
 
     @Presubmit
     @Test
-    fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp)
+    fun primaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(secondaryApp)
+    fun secondaryAppWindowBecomesInvisible() = flicker.appWindowBecomesInvisible(secondaryApp)
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 251268711)
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 206753786)
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Presubmit
@@ -137,26 +129,22 @@
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @FlakyTest
@@ -173,8 +161,8 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index 8d771fe..5b656b3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -21,10 +21,10 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
 import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
@@ -50,35 +50,31 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class DragDividerToResize(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class DragDividerToResize(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
-            setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
-            }
-            transitions {
-                SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper)
-            }
+            setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
+            transitions { SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper) }
         }
 
     @Before
     fun before() {
-        Assume.assumeTrue(tapl.isTablet || !testSpec.isLandscapeOrSeascapeAtStart)
+        Assume.assumeTrue(tapl.isTablet || !flicker.scenario.isLandscapeOrSeascapeAtStart)
     }
 
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
     fun cujCompleted() {
-        testSpec.appWindowIsVisibleAtStart(primaryApp)
-        testSpec.appWindowIsVisibleAtStart(secondaryApp)
-        testSpec.splitScreenDividerIsVisibleAtStart()
+        flicker.appWindowIsVisibleAtStart(primaryApp)
+        flicker.appWindowIsVisibleAtStart(secondaryApp)
+        flicker.splitScreenDividerIsVisibleAtStart()
 
-        testSpec.appWindowIsVisibleAtEnd(primaryApp)
-        testSpec.appWindowIsVisibleAtEnd(secondaryApp)
-        testSpec.splitScreenDividerIsVisibleAtEnd()
+        flicker.appWindowIsVisibleAtEnd(primaryApp)
+        flicker.appWindowIsVisibleAtEnd(secondaryApp)
+        flicker.splitScreenDividerIsVisibleAtEnd()
 
         // TODO(b/246490534): Add validation for resized app after withAppTransitionIdle is
         // robust enough to get the correct end state.
@@ -86,16 +82,14 @@
 
     @Presubmit
     @Test
-    fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+    fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
 
-    @Presubmit
-    @Test
-    fun primaryAppLayerKeepVisible() = testSpec.layerKeepVisible(primaryApp)
+    @Presubmit @Test fun primaryAppLayerKeepVisible() = flicker.layerKeepVisible(primaryApp)
 
     @Presubmit
     @Test
     fun secondaryAppLayerVisibilityChanges() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(secondaryApp)
                 .then()
                 .isInvisible(secondaryApp)
@@ -104,53 +98,47 @@
         }
     }
 
-    @Presubmit
-    @Test
-    fun primaryAppWindowKeepVisible() = testSpec.appWindowKeepVisible(primaryApp)
+    @Presubmit @Test fun primaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowKeepVisible() = testSpec.appWindowKeepVisible(secondaryApp)
+    fun secondaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(secondaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppBoundsChanges() = testSpec.splitAppLayerBoundsChanges(
-        primaryApp,
-        landscapePosLeft = true,
-        portraitPosTop = false
-    )
+    fun primaryAppBoundsChanges() =
+        flicker.splitAppLayerBoundsChanges(
+            primaryApp,
+            landscapePosLeft = true,
+            portraitPosTop = false
+        )
 
     @FlakyTest(bugId = 250530664)
     @Test
-    fun secondaryAppBoundsChanges() = testSpec.splitAppLayerBoundsChanges(
-        secondaryApp,
-        landscapePosLeft = false,
-        portraitPosTop = true
-    )
+    fun secondaryAppBoundsChanges() =
+        flicker.splitAppLayerBoundsChanges(
+            secondaryApp,
+            landscapePosLeft = false,
+            portraitPosTop = true
+        )
+
+    /** {@inheritDoc} */
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -161,26 +149,22 @@
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -197,8 +181,8 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
index 7378e21..4e36c36 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
@@ -19,13 +19,13 @@
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowBecomesVisible
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -44,8 +44,8 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test enter split screen by dragging app icon from all apps.
- * This test is only for large screen devices.
+ * Test enter split screen by dragging app icon from all apps. This test is only for large screen
+ * devices.
  *
  * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromAllApps`
  */
@@ -53,9 +53,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenByDragFromAllApps(
-    testSpec: FlickerTestParameter
-) : SplitScreenBase(testSpec) {
+class EnterSplitScreenByDragFromAllApps(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     @Before
     fun before() {
@@ -71,9 +69,9 @@
             }
             transitions {
                 tapl.launchedAppState.taskbar
-                        .openAllApps()
-                        .getAppIcon(secondaryApp.appName)
-                        .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
+                    .openAllApps()
+                    .getAppIcon(secondaryApp.appName)
+                    .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
                 SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
             }
         }
@@ -81,13 +79,13 @@
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = false)
+    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = false)
 
     @Presubmit
     @Test
     fun splitScreenDividerBecomesVisible() {
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.splitScreenDividerBecomesVisible()
+        flicker.splitScreenDividerBecomesVisible()
     }
 
     // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready.
@@ -95,60 +93,54 @@
     @Test
     fun splitScreenDividerIsVisibleAtEnd_ShellTransit() {
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.assertLayersEnd {
-            this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
-        }
+        flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
     }
 
-    @Presubmit
-    @Test
-    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
+    @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+    fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
-        primaryApp, landscapePosLeft = false, portraitPosTop = false)
+    fun primaryAppBoundsIsVisibleAtEnd() =
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            primaryApp,
+            landscapePosLeft = false,
+            portraitPosTop = false
+        )
 
     @Presubmit
     @Test
-    fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisibleByDrag(
-        secondaryApp)
+    fun secondaryAppBoundsBecomesVisible() =
+        flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
+    fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+    fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+    /** {@inheritDoc} */
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -159,26 +151,22 @@
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -195,11 +183,11 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
-                supportedNavigationModes =
-                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY))
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
index 0c03d31..5d37e85 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
@@ -19,13 +19,13 @@
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
 import com.android.wm.shell.flicker.layerBecomesVisible
@@ -43,8 +43,8 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test enter split screen by dragging app icon from notification.
- * This test is only for large screen devices.
+ * Test enter split screen by dragging app icon from notification. This test is only for large
+ * screen devices.
  *
  * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromNotification`
  */
@@ -52,9 +52,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenByDragFromNotification(
-    testSpec: FlickerTestParameter
-) : SplitScreenBase(testSpec) {
+class EnterSplitScreenByDragFromNotification(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     private val sendNotificationApp = SplitScreenUtils.getSendNotification(instrumentation)
 
@@ -78,21 +76,19 @@
                 SplitScreenUtils.dragFromNotificationToSplit(instrumentation, device, wmHelper)
                 SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp)
             }
-            teardown {
-                sendNotificationApp.exit(wmHelper)
-            }
+            teardown { sendNotificationApp.exit(wmHelper) }
         }
 
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = false)
+    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = false)
 
     @Presubmit
     @Test
     fun splitScreenDividerBecomesVisible() {
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.splitScreenDividerBecomesVisible()
+        flicker.splitScreenDividerBecomesVisible()
     }
 
     // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready.
@@ -100,20 +96,16 @@
     @Test
     fun splitScreenDividerIsVisibleAtEnd_ShellTransit() {
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.assertLayersEnd {
-            this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
-        }
+        flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
     }
 
-    @Presubmit
-    @Test
-    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
+    @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
     fun secondaryAppLayerBecomesVisible() {
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isInvisible(sendNotificationApp)
                 .then()
                 .isVisible(sendNotificationApp)
@@ -129,50 +121,48 @@
     @Test
     fun secondaryAppLayerBecomesVisible_ShellTransit() {
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.layerBecomesVisible(sendNotificationApp)
+        flicker.layerBecomesVisible(sendNotificationApp)
     }
 
     @Presubmit
     @Test
-    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
-        primaryApp, landscapePosLeft = false, portraitPosTop = false)
+    fun primaryAppBoundsIsVisibleAtEnd() =
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            primaryApp,
+            landscapePosLeft = false,
+            portraitPosTop = false
+        )
 
     @Presubmit
     @Test
-    fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisibleByDrag(
-        sendNotificationApp)
+    fun secondaryAppBoundsBecomesVisible() =
+        flicker.splitAppLayerBoundsBecomesVisibleByDrag(sendNotificationApp)
 
     @Presubmit
     @Test
-    fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
+    fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(sendNotificationApp)
+    fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(sendNotificationApp)
+
+    /** {@inheritDoc} */
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -183,26 +173,22 @@
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -219,11 +205,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
-                supportedNavigationModes =
-                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
             )
         }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
index dcadb5a..abf9426 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
@@ -17,14 +17,14 @@
 package com.android.wm.shell.flicker.splitscreen
 
 import android.platform.test.annotations.IwTest
-import android.view.WindowManagerPolicyConstants
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
 import com.android.wm.shell.flicker.layerBecomesVisible
 import com.android.wm.shell.flicker.layerIsVisibleAtEnd
@@ -41,8 +41,7 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test enter split screen by dragging a shortcut.
- * This test is only for large screen devices.
+ * Test enter split screen by dragging a shortcut. This test is only for large screen devices.
  *
  * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromShortcut`
  */
@@ -50,13 +49,11 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenByDragFromShortcut(
-    testSpec: FlickerTestParameter
-) : SplitScreenBase(testSpec) {
+class EnterSplitScreenByDragFromShortcut(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     @Before
     fun before() {
-        Assume.assumeTrue(testSpec.isTablet)
+        Assume.assumeTrue(flicker.scenario.isTablet)
     }
 
     override val transition: FlickerBuilder.() -> Unit
@@ -80,39 +77,46 @@
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp,
-        fromOtherApp = false, appExistAtStart = false)
+    fun cujCompleted() =
+        flicker.splitScreenEntered(
+            primaryApp,
+            secondaryApp,
+            fromOtherApp = false,
+            appExistAtStart = false
+        )
 
     @Presubmit
     @Test
-    fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+    fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
+
+    @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
+    fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+    fun primaryAppBoundsIsVisibleAtEnd() =
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            primaryApp,
+            landscapePosLeft = false,
+            portraitPosTop = false
+        )
 
     @Presubmit
     @Test
-    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
-        primaryApp, landscapePosLeft = false, portraitPosTop = false)
+    fun secondaryAppBoundsBecomesVisible() =
+        flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisibleByDrag(
-        secondaryApp)
-
-    @Presubmit
-    @Test
-    fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
+    fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
     fun secondaryAppWindowBecomesVisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.notContains(secondaryApp)
                 .then()
                 .isAppWindowInvisible(secondaryApp, isOptional = true)
@@ -122,28 +126,22 @@
     }
 
     /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -154,26 +152,22 @@
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -190,11 +184,11 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
-                supportedNavigationModes =
-                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY))
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
+            )
         }
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
index 496d439..795a2c4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
@@ -19,13 +19,13 @@
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowBecomesVisible
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -44,8 +44,8 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test enter split screen by dragging app icon from taskbar.
- * This test is only for large screen devices.
+ * Test enter split screen by dragging app icon from taskbar. This test is only for large screen
+ * devices.
  *
  * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromTaskbar`
  */
@@ -53,9 +53,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenByDragFromTaskbar(
-    testSpec: FlickerTestParameter
-) : SplitScreenBase(testSpec) {
+class EnterSplitScreenByDragFromTaskbar(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     @Before
     fun before() {
@@ -68,9 +66,7 @@
             super.transition(this)
             setup {
                 tapl.goHome()
-                SplitScreenUtils.createShortcutOnHotseatIfNotExist(
-                    tapl, secondaryApp.appName
-                )
+                SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName)
                 primaryApp.launchViaIntent(wmHelper)
             }
             transitions {
@@ -84,13 +80,13 @@
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = false)
+    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = false)
 
     @Presubmit
     @Test
     fun splitScreenDividerBecomesVisible() {
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.splitScreenDividerBecomesVisible()
+        flicker.splitScreenDividerBecomesVisible()
     }
 
     // TODO(b/245472831): Back to splitScreenDividerBecomesVisible after shell transition ready.
@@ -98,20 +94,16 @@
     @Test
     fun splitScreenDividerIsVisibleAtEnd_ShellTransit() {
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.assertLayersEnd {
-            this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
-        }
+        flicker.assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
     }
 
-    @Presubmit
-    @Test
-    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
+    @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
     fun secondaryAppLayerBecomesVisible() {
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isInvisible(secondaryApp)
                 .then()
                 .isVisible(secondaryApp)
@@ -127,50 +119,48 @@
     @Test
     fun secondaryAppLayerBecomesVisible_ShellTransit() {
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.layerBecomesVisible(secondaryApp)
+        flicker.layerBecomesVisible(secondaryApp)
     }
 
     @Presubmit
     @Test
-    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
-        primaryApp, landscapePosLeft = false, portraitPosTop = false)
+    fun primaryAppBoundsIsVisibleAtEnd() =
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            primaryApp,
+            landscapePosLeft = false,
+            portraitPosTop = false
+        )
 
     @Presubmit
     @Test
-    fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisibleByDrag(
-        secondaryApp)
+    fun secondaryAppBoundsBecomesVisible() =
+        flicker.splitAppLayerBoundsBecomesVisibleByDrag(secondaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
+    fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+    fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
+
+    /** {@inheritDoc} */
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -181,26 +171,22 @@
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -217,10 +203,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
-                supportedNavigationModes =
-                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
             )
         }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
index fb7b8b7..c09ca91 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -21,11 +21,11 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.wm.shell.flicker.appWindowBecomesVisible
 import com.android.wm.shell.flicker.layerBecomesVisible
 import com.android.wm.shell.flicker.layerIsVisibleAtEnd
@@ -49,7 +49,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class EnterSplitScreenFromOverview(flicker: FlickerTest) : SplitScreenBase(flicker) {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
@@ -57,7 +57,8 @@
                 primaryApp.launchViaIntent(wmHelper)
                 secondaryApp.launchViaIntent(wmHelper)
                 tapl.goHome()
-                wmHelper.StateSyncBuilder()
+                wmHelper
+                    .StateSyncBuilder()
                     .withAppTransitionIdle()
                     .withHomeActivityVisible()
                     .waitForAndVerify()
@@ -71,72 +72,76 @@
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
+    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
 
     @Presubmit
     @Test
-    fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+    fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
+
+    @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
+    fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
-
-    @Presubmit
-    @Test
-    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
-        primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false)
+    fun primaryAppBoundsIsVisibleAtEnd() =
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            primaryApp,
+            landscapePosLeft = tapl.isTablet,
+            portraitPosTop = false
+        )
 
     @Presubmit
     @Test
     fun secondaryAppBoundsBecomesVisible() {
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.splitAppLayerBoundsBecomesVisible(
-            secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true)
+        flicker.splitAppLayerBoundsBecomesVisible(
+            secondaryApp,
+            landscapePosLeft = !tapl.isTablet,
+            portraitPosTop = true
+        )
     }
 
     @FlakyTest(bugId = 244407465)
     @Test
     fun secondaryAppBoundsBecomesVisible_shellTransit() {
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.splitAppLayerBoundsBecomesVisible(
-            secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true)
+        flicker.splitAppLayerBoundsBecomesVisible(
+            secondaryApp,
+            landscapePosLeft = !tapl.isTablet,
+            portraitPosTop = true
+        )
     }
 
     @Presubmit
     @Test
-    fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp)
+    fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+    fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 251269324)
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Presubmit
@@ -147,26 +152,22 @@
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 252736515)
@@ -183,8 +184,8 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
index c841333..8c0a303 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
@@ -17,12 +17,12 @@
 package com.android.wm.shell.flicker.splitscreen
 
 import android.content.Context
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.wm.shell.flicker.BaseTest
 
-abstract class SplitScreenBase(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+abstract class SplitScreenBase(flicker: FlickerTest) : BaseTest(flicker) {
     protected val context: Context = instrumentation.context
     protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
     protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
@@ -32,8 +32,8 @@
         get() = {
             setup {
                 tapl.setEnableRotation(true)
-                setRotation(testSpec.startRotation)
-                tapl.setExpectedRotation(testSpec.startRotation)
+                setRotation(flicker.scenario.startRotation)
+                tapl.setExpectedRotation(flicker.scenario.startRotation.value)
                 tapl.workspace.switchToOverview().dismissAllTasks()
             }
             teardown {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
index 4a3284e..f3927d4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
@@ -41,8 +41,8 @@
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
 import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import org.junit.Assert.assertNotNull
 import java.util.Collections
+import org.junit.Assert.assertNotNull
 
 internal object SplitScreenUtils {
     private const val TIMEOUT_MS = 3_000L
@@ -129,10 +129,18 @@
 
             // Find the second task in the upper right corner in split select mode by sorting
             // 'left' in descending order and 'top' in ascending order.
-            Collections.sort(snapshots, { t1: UiObject2, t2: UiObject2 ->
-                t2.getVisibleBounds().left - t1.getVisibleBounds().left})
-            Collections.sort(snapshots, { t1: UiObject2, t2: UiObject2 ->
-                t1.getVisibleBounds().top - t2.getVisibleBounds().top})
+            Collections.sort(
+                snapshots,
+                { t1: UiObject2, t2: UiObject2 ->
+                    t2.getVisibleBounds().left - t1.getVisibleBounds().left
+                }
+            )
+            Collections.sort(
+                snapshots,
+                { t1: UiObject2, t2: UiObject2 ->
+                    t1.getVisibleBounds().top - t2.getVisibleBounds().top
+                }
+            )
             snapshots[0].click()
         } else {
             tapl.workspace
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index f7610c4..09568b2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -19,13 +19,13 @@
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -50,19 +50,15 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchAppByDoubleTapDivider(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class SwitchAppByDoubleTapDivider(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
-            setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
-            }
+            setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
             transitions {
                 SplitScreenUtils.doubleTapDividerToSwitch(device)
-                wmHelper.StateSyncBuilder()
-                    .withAppTransitionIdle()
-                    .waitForAndVerify()
+                wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
 
                 waitForLayersToSwitch(wmHelper)
                 waitForWindowsToSwitch(wmHelper)
@@ -70,61 +66,74 @@
         }
 
     private fun waitForWindowsToSwitch(wmHelper: WindowManagerStateHelper) {
-        wmHelper.StateSyncBuilder().add("appWindowsSwitched") {
-            val primaryAppWindow = it.wmState.visibleWindows.firstOrNull { window ->
-                primaryApp.windowMatchesAnyOf(window)
-            } ?: return@add false
-            val secondaryAppWindow = it.wmState.visibleWindows.firstOrNull { window ->
-                secondaryApp.windowMatchesAnyOf(window)
-            } ?: return@add false
+        wmHelper
+            .StateSyncBuilder()
+            .add("appWindowsSwitched") {
+                val primaryAppWindow =
+                    it.wmState.visibleWindows.firstOrNull { window ->
+                        primaryApp.windowMatchesAnyOf(window)
+                    }
+                        ?: return@add false
+                val secondaryAppWindow =
+                    it.wmState.visibleWindows.firstOrNull { window ->
+                        secondaryApp.windowMatchesAnyOf(window)
+                    }
+                        ?: return@add false
 
-            if (isLandscape(testSpec.endRotation)) {
-                return@add if (testSpec.isTablet) {
-                    secondaryAppWindow.frame.right <= primaryAppWindow.frame.left
+                if (isLandscape(flicker.scenario.endRotation)) {
+                    return@add if (flicker.scenario.isTablet) {
+                        secondaryAppWindow.frame.right <= primaryAppWindow.frame.left
+                    } else {
+                        primaryAppWindow.frame.right <= secondaryAppWindow.frame.left
+                    }
                 } else {
-                    primaryAppWindow.frame.right <= secondaryAppWindow.frame.left
-                }
-            } else {
-                return@add if (testSpec.isTablet) {
-                    primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
-                } else {
-                    primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+                    return@add if (flicker.scenario.isTablet) {
+                        primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+                    } else {
+                        primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+                    }
                 }
             }
-        }.waitForAndVerify()
+            .waitForAndVerify()
     }
 
     private fun waitForLayersToSwitch(wmHelper: WindowManagerStateHelper) {
-        wmHelper.StateSyncBuilder().add("appLayersSwitched") {
-            val primaryAppLayer = it.layerState.visibleLayers.firstOrNull { window ->
-                primaryApp.layerMatchesAnyOf(window)
-            } ?: return@add false
-            val secondaryAppLayer = it.layerState.visibleLayers.firstOrNull { window ->
-                secondaryApp.layerMatchesAnyOf(window)
-            } ?: return@add false
+        wmHelper
+            .StateSyncBuilder()
+            .add("appLayersSwitched") {
+                val primaryAppLayer =
+                    it.layerState.visibleLayers.firstOrNull { window ->
+                        primaryApp.layerMatchesAnyOf(window)
+                    }
+                        ?: return@add false
+                val secondaryAppLayer =
+                    it.layerState.visibleLayers.firstOrNull { window ->
+                        secondaryApp.layerMatchesAnyOf(window)
+                    }
+                        ?: return@add false
 
-            val primaryVisibleRegion = primaryAppLayer.visibleRegion?.bounds
-                ?: return@add false
-            val secondaryVisibleRegion = secondaryAppLayer.visibleRegion?.bounds
-                ?: return@add false
+                val primaryVisibleRegion = primaryAppLayer.visibleRegion?.bounds ?: return@add false
+                val secondaryVisibleRegion =
+                    secondaryAppLayer.visibleRegion?.bounds ?: return@add false
 
-            if (isLandscape(testSpec.endRotation)) {
-                return@add if (testSpec.isTablet) {
-                    secondaryVisibleRegion.right <= primaryVisibleRegion.left
+                if (isLandscape(flicker.scenario.endRotation)) {
+                    return@add if (flicker.scenario.isTablet) {
+                        secondaryVisibleRegion.right <= primaryVisibleRegion.left
+                    } else {
+                        primaryVisibleRegion.right <= secondaryVisibleRegion.left
+                    }
                 } else {
-                    primaryVisibleRegion.right <= secondaryVisibleRegion.left
-                }
-            } else {
-                return@add if (testSpec.isTablet) {
-                    primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
-                } else {
-                    primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+                    return@add if (flicker.scenario.isTablet) {
+                        primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+                    } else {
+                        primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+                    }
                 }
             }
-        }.waitForAndVerify()
+            .waitForAndVerify()
     }
 
-    private fun isLandscape(rotation: Int): Boolean {
+    private fun isLandscape(rotation: PlatformConsts.Rotation): Boolean {
         val displayBounds = WindowUtils.getDisplayBounds(rotation)
         return displayBounds.width > displayBounds.height
     }
@@ -133,13 +142,13 @@
     @Presubmit
     @Test
     fun cujCompleted() {
-        testSpec.appWindowIsVisibleAtStart(primaryApp)
-        testSpec.appWindowIsVisibleAtStart(secondaryApp)
-        testSpec.splitScreenDividerIsVisibleAtStart()
+        flicker.appWindowIsVisibleAtStart(primaryApp)
+        flicker.appWindowIsVisibleAtStart(secondaryApp)
+        flicker.splitScreenDividerIsVisibleAtStart()
 
-        testSpec.appWindowIsVisibleAtEnd(primaryApp)
-        testSpec.appWindowIsVisibleAtEnd(secondaryApp)
-        testSpec.splitScreenDividerIsVisibleAtEnd()
+        flicker.appWindowIsVisibleAtEnd(primaryApp)
+        flicker.appWindowIsVisibleAtEnd(secondaryApp)
+        flicker.splitScreenDividerIsVisibleAtEnd()
 
         // TODO(b/246490534): Add validation for switched app after withAppTransitionIdle is
         // robust enough to get the correct end state.
@@ -147,63 +156,57 @@
 
     @Presubmit
     @Test
-    fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+    fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+
+    @Presubmit @Test fun primaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
+    fun secondaryAppLayerIsVisibleAtEnd() = flicker.layerIsVisibleAtEnd(secondaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp)
+    fun primaryAppBoundsIsVisibleAtEnd() =
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            primaryApp,
+            landscapePosLeft = !tapl.isTablet,
+            portraitPosTop = true
+        )
 
     @Presubmit
     @Test
-    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
-        primaryApp,
-        landscapePosLeft = !tapl.isTablet,
-        portraitPosTop = true
-    )
+    fun secondaryAppBoundsIsVisibleAtEnd() =
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            secondaryApp,
+            landscapePosLeft = tapl.isTablet,
+            portraitPosTop = false
+        )
 
     @Presubmit
     @Test
-    fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
-        secondaryApp,
-        landscapePosLeft = tapl.isTablet,
-        portraitPosTop = false
-    )
+    fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
 
     @Presubmit
     @Test
-    fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
+    fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp)
 
-    @Presubmit
-    @Test
-    fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp)
+    /** {@inheritDoc} */
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -214,26 +217,22 @@
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Postsubmit
@@ -250,11 +249,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
-                supportedNavigationModes =
-                listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
             )
         }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
index 993dba2..940e0e9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
@@ -19,12 +19,12 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.appWindowBecomesVisible
 import com.android.wm.shell.flicker.layerBecomesVisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
@@ -45,7 +45,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class SwitchBackToSplitFromAnotherApp(flicker: FlickerTest) : SplitScreenBase(flicker) {
     val thirdApp = SplitScreenUtils.getNonResizeable(instrumentation)
 
     override val transition: FlickerBuilder.() -> Unit
@@ -66,22 +66,22 @@
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
+    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
 
     @Presubmit
     @Test
-    fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+    fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
 
-    @Presubmit @Test fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp)
+    @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+    fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
 
     @Presubmit
     @Test
     fun primaryAppBoundsIsVisibleAtEnd() =
-        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
             primaryApp,
             landscapePosLeft = tapl.isTablet,
             portraitPosTop = false
@@ -90,7 +90,7 @@
     @Presubmit
     @Test
     fun secondaryAppBoundsIsVisibleAtEnd() =
-        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
             secondaryApp,
             landscapePosLeft = !tapl.isTablet,
             portraitPosTop = true
@@ -98,17 +98,14 @@
 
     @Presubmit
     @Test
-    fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp)
+    fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+    fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
 
     /** {@inheritDoc} */
-    @FlakyTest
-    @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Presubmit
@@ -166,13 +163,11 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
-                    supportedNavigationModes =
-                        listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
-                )
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
index 2a552cd..85812c4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
@@ -19,12 +19,12 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.appWindowBecomesVisible
 import com.android.wm.shell.flicker.layerBecomesVisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
@@ -45,7 +45,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class SwitchBackToSplitFromHome(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -65,22 +65,22 @@
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
+    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
 
     @Presubmit
     @Test
-    fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+    fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
 
-    @Presubmit @Test fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp)
+    @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+    fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
 
     @Presubmit
     @Test
     fun primaryAppBoundsIsVisibleAtEnd() =
-        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
             primaryApp,
             landscapePosLeft = tapl.isTablet,
             portraitPosTop = false
@@ -89,7 +89,7 @@
     @Presubmit
     @Test
     fun secondaryAppBoundsIsVisibleAtEnd() =
-        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
             secondaryApp,
             landscapePosLeft = !tapl.isTablet,
             portraitPosTop = true
@@ -97,17 +97,14 @@
 
     @Presubmit
     @Test
-    fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp)
+    fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+    fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
 
     /** {@inheritDoc} */
-    @FlakyTest
-    @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Presubmit
@@ -165,13 +162,11 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
-                    supportedNavigationModes =
-                        listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
-                )
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
index 7f81bae..7c62433 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
@@ -19,12 +19,12 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.appWindowBecomesVisible
 import com.android.wm.shell.flicker.layerBecomesVisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
@@ -45,7 +45,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class SwitchBackToSplitFromRecent(flicker: FlickerTest) : SplitScreenBase(flicker) {
 
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -65,22 +65,22 @@
     @IwTest(focusArea = "sysui")
     @Presubmit
     @Test
-    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
+    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
 
     @Presubmit
     @Test
-    fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+    fun splitScreenDividerBecomesVisible() = flicker.splitScreenDividerBecomesVisible()
 
-    @Presubmit @Test fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp)
+    @Presubmit @Test fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+    fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
 
     @Presubmit
     @Test
     fun primaryAppBoundsIsVisibleAtEnd() =
-        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
             primaryApp,
             landscapePosLeft = tapl.isTablet,
             portraitPosTop = false
@@ -89,7 +89,7 @@
     @Presubmit
     @Test
     fun secondaryAppBoundsIsVisibleAtEnd() =
-        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
             secondaryApp,
             landscapePosLeft = !tapl.isTablet,
             portraitPosTop = true
@@ -97,17 +97,14 @@
 
     @Presubmit
     @Test
-    fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp)
+    fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+    fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
 
     /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Presubmit
@@ -165,13 +162,11 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
-                    supportedNavigationModes =
-                        listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
-                )
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
+            )
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
index f5f5fd8..193ab98 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
@@ -20,22 +20,22 @@
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowBecomesInvisible
 import com.android.wm.shell.flicker.appWindowBecomesVisible
 import com.android.wm.shell.flicker.appWindowIsInvisibleAtEnd
-import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
 import com.android.wm.shell.flicker.layerBecomesInvisible
 import com.android.wm.shell.flicker.layerBecomesVisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
 import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -51,7 +51,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchBetweenSplitPairs(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+class SwitchBetweenSplitPairs(flicker: FlickerTest) : SplitScreenBase(flicker) {
     private val thirdApp = SplitScreenUtils.getIme(instrumentation)
     private val fourthApp = SplitScreenUtils.getSendNotification(instrumentation)
 
@@ -77,21 +77,21 @@
     @Presubmit
     @Test
     fun cujCompleted() {
-        testSpec.appWindowIsVisibleAtStart(thirdApp)
-        testSpec.appWindowIsVisibleAtStart(fourthApp)
-        testSpec.splitScreenDividerIsVisibleAtStart()
+        flicker.appWindowIsVisibleAtStart(thirdApp)
+        flicker.appWindowIsVisibleAtStart(fourthApp)
+        flicker.splitScreenDividerIsVisibleAtStart()
 
-        testSpec.appWindowIsVisibleAtEnd(primaryApp)
-        testSpec.appWindowIsVisibleAtEnd(secondaryApp)
-        testSpec.appWindowIsInvisibleAtEnd(thirdApp)
-        testSpec.appWindowIsInvisibleAtEnd(fourthApp)
-        testSpec.splitScreenDividerIsVisibleAtEnd()
+        flicker.appWindowIsVisibleAtEnd(primaryApp)
+        flicker.appWindowIsVisibleAtEnd(secondaryApp)
+        flicker.appWindowIsInvisibleAtEnd(thirdApp)
+        flicker.appWindowIsInvisibleAtEnd(fourthApp)
+        flicker.splitScreenDividerIsVisibleAtEnd()
     }
 
     @Presubmit
     @Test
     fun splitScreenDividerInvisibleAtMiddle() =
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
                 .then()
                 .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
@@ -101,24 +101,24 @@
 
     @FlakyTest(bugId = 247095572)
     @Test
-    fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp)
+    fun primaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(primaryApp)
 
     @FlakyTest(bugId = 247095572)
     @Test
-    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+    fun secondaryAppLayerBecomesVisible() = flicker.layerBecomesVisible(secondaryApp)
 
     @FlakyTest(bugId = 247095572)
     @Test
-    fun thirdAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(thirdApp)
+    fun thirdAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(thirdApp)
 
     @FlakyTest(bugId = 247095572)
     @Test
-    fun fourthAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(fourthApp)
+    fun fourthAppLayerBecomesInvisible() = flicker.layerBecomesInvisible(fourthApp)
 
     @Presubmit
     @Test
     fun primaryAppBoundsIsVisibleAtEnd() =
-        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
             primaryApp,
             landscapePosLeft = tapl.isTablet,
             portraitPosTop = false
@@ -127,7 +127,7 @@
     @Presubmit
     @Test
     fun secondaryAppBoundsIsVisibleAtEnd() =
-        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
             secondaryApp,
             landscapePosLeft = !tapl.isTablet,
             portraitPosTop = true
@@ -136,66 +136,62 @@
     @Presubmit
     @Test
     fun thirdAppBoundsIsVisibleAtBegin() =
-        testSpec.assertLayersStart {
+        flicker.assertLayersStart {
             this.splitAppLayerBoundsSnapToDivider(
                 thirdApp,
                 landscapePosLeft = tapl.isTablet,
                 portraitPosTop = false,
-                testSpec.startRotation
+                flicker.scenario.startRotation
             )
         }
 
     @Presubmit
     @Test
     fun fourthAppBoundsIsVisibleAtBegin() =
-        testSpec.assertLayersStart {
+        flicker.assertLayersStart {
             this.splitAppLayerBoundsSnapToDivider(
                 fourthApp,
                 landscapePosLeft = !tapl.isTablet,
                 portraitPosTop = true,
-                testSpec.startRotation
+                flicker.scenario.startRotation
             )
         }
 
     @Presubmit
     @Test
-    fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp)
+    fun primaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(primaryApp)
 
     @Presubmit
     @Test
-    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+    fun secondaryAppWindowBecomesVisible() = flicker.appWindowBecomesVisible(secondaryApp)
 
     @Presubmit
     @Test
-    fun thirdAppWindowBecomesVisible() = testSpec.appWindowBecomesInvisible(thirdApp)
+    fun thirdAppWindowBecomesVisible() = flicker.appWindowBecomesInvisible(thirdApp)
 
     @Presubmit
     @Test
-    fun fourthAppWindowBecomesVisible() = testSpec.appWindowBecomesInvisible(fourthApp)
+    fun fourthAppWindowBecomesVisible() = flicker.appWindowBecomesInvisible(fourthApp)
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 251268711)
     @Test
-    override fun entireScreenCovered() =
-        super.entireScreenCovered()
+    override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 206753786)
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() =
-        super.navBarLayerPositionAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun navBarWindowIsAlwaysVisible() =
-        super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Presubmit
@@ -206,26 +202,22 @@
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun statusBarLayerPositionAtStartAndEnd() =
-        super.statusBarLayerPositionAtStartAndEnd()
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun statusBarWindowIsAlwaysVisible() =
-        super.statusBarWindowIsAlwaysVisible()
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() =
-        super.taskBarLayerIsVisibleAtStartAndEnd()
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun taskBarWindowIsAlwaysVisible() =
-        super.taskBarWindowIsAlwaysVisible()
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @FlakyTest
@@ -242,8 +234,8 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index bee9a90..8a5b490 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -53,6 +53,7 @@
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 import android.window.BackNavigationInfo;
 import android.window.IBackAnimationFinishedCallback;
 import android.window.IOnBackInvokedCallback;
@@ -223,9 +224,10 @@
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
-        verify(mAnimatorCallback).onBackStarted(any(BackEvent.class));
+        verify(mAnimatorCallback).onBackStarted(any(BackMotionEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
-        ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+        ArgumentCaptor<BackMotionEvent> backEventCaptor =
+                ArgumentCaptor.forClass(BackMotionEvent.class);
         verify(mAnimatorCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
 
         // Check that back invocation is dispatched.
@@ -246,7 +248,8 @@
         shellInit.init();
         registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
-        ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+        ArgumentCaptor<BackMotionEvent> backEventCaptor =
+                ArgumentCaptor.forClass(BackMotionEvent.class);
 
         createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, false);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
index 3aefc3f..ba9c159 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 
 import android.window.BackEvent;
+import android.window.BackMotionEvent;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -38,7 +39,7 @@
     @Test
     public void generatesProgress_onStart() {
         mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
-        BackEvent event = mTouchTracker.createStartEvent(null);
+        BackMotionEvent event = mTouchTracker.createStartEvent(null);
         assertEquals(event.getProgress(), 0f, 0f);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index 89bafcb..dad9133 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -36,7 +36,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -50,6 +50,7 @@
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 import android.window.WindowContainerTransaction.Change;
+import android.window.WindowContainerTransaction.HierarchyOp;
 
 import androidx.test.filters.SmallTest;
 
@@ -99,15 +100,14 @@
     @Before
     public void setUp() {
         mMockitoSession = mockitoSession().mockStatic(DesktopModeStatus.class).startMocking();
+        when(DesktopModeStatus.isSupported()).thenReturn(true);
         when(DesktopModeStatus.isActive(any())).thenReturn(true);
 
         mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
 
         mDesktopModeTaskRepository = new DesktopModeTaskRepository();
 
-        mController = new DesktopModeController(mContext, mShellInit, mShellController,
-                mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions,
-                mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor());
+        mController = createController();
 
         when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>());
 
@@ -124,7 +124,17 @@
 
     @Test
     public void instantiate_addInitCallback() {
-        verify(mShellInit, times(1)).addInitCallback(any(), any());
+        verify(mShellInit).addInitCallback(any(), any());
+    }
+
+    @Test
+    public void instantiate_flagOff_doNotAddInitCallback() {
+        when(DesktopModeStatus.isSupported()).thenReturn(false);
+        clearInvocations(mShellInit);
+
+        createController();
+
+        verify(mShellInit, never()).addInitCallback(any(), any());
     }
 
     @Test
@@ -222,25 +232,29 @@
         // Check that there are hierarchy changes for home task and visible task
         assertThat(wct.getHierarchyOps()).hasSize(2);
         // First show home task
-        WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
+        HierarchyOp op1 = wct.getHierarchyOps().get(0);
         assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
         assertThat(op1.getContainer()).isEqualTo(homeTask.token.asBinder());
 
         // Then visible task on top of it
-        WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1);
+        HierarchyOp op2 = wct.getHierarchyOps().get(1);
         assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
         assertThat(op2.getContainer()).isEqualTo(fullscreenTask1.token.asBinder());
     }
 
     @Test
-    public void testShowDesktopApps() {
-        // Set up two active tasks on desktop
+    public void testShowDesktopApps_allAppsInvisible_bringsToFront() {
+        // Set up two active tasks on desktop, task2 is on top of task1.
         RunningTaskInfo freeformTask1 = createFreeformTask();
-        freeformTask1.lastActiveTime = 100;
-        RunningTaskInfo freeformTask2 = createFreeformTask();
-        freeformTask2.lastActiveTime = 200;
         mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(freeformTask1.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(
+                freeformTask1.taskId, false /* visible */);
+        RunningTaskInfo freeformTask2 = createFreeformTask();
         mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(freeformTask2.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(
+                freeformTask2.taskId, false /* visible */);
         when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn(
                 freeformTask1);
         when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn(
@@ -248,27 +262,66 @@
 
         // Run show desktop apps logic
         mController.showDesktopApps();
-        ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(
-                WindowContainerTransaction.class);
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), wctCaptor.capture(), any());
-        } else {
-            verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture());
-        }
-        WindowContainerTransaction wct = wctCaptor.getValue();
 
+        final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
         // Check wct has reorder calls
         assertThat(wct.getHierarchyOps()).hasSize(2);
 
-        // Task 2 has activity later, must be first
-        WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
+        // Task 1 appeared first, must be first reorder to top.
+        HierarchyOp op1 = wct.getHierarchyOps().get(0);
         assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
-        assertThat(op1.getContainer()).isEqualTo(freeformTask2.token.asBinder());
+        assertThat(op1.getContainer()).isEqualTo(freeformTask1.token.asBinder());
 
-        // Task 1 should be second
-        WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1);
+        // Task 2 appeared last, must be last reorder to top.
+        HierarchyOp op2 = wct.getHierarchyOps().get(1);
         assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
-        assertThat(op2.getContainer()).isEqualTo(freeformTask1.token.asBinder());
+        assertThat(op2.getContainer()).isEqualTo(freeformTask2.token.asBinder());
+    }
+
+    @Test
+    public void testShowDesktopApps_appsAlreadyVisible_doesNothing() {
+        final RunningTaskInfo task1 = createFreeformTask();
+        mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+        when(mShellTaskOrganizer.getRunningTaskInfo(task1.taskId)).thenReturn(task1);
+        final RunningTaskInfo task2 = createFreeformTask();
+        mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+        when(mShellTaskOrganizer.getRunningTaskInfo(task2.taskId)).thenReturn(task2);
+
+        mController.showDesktopApps();
+
+        final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
+        // No reordering needed.
+        assertThat(wct.getHierarchyOps()).isEmpty();
+    }
+
+    @Test
+    public void testShowDesktopApps_someAppsInvisible_reordersAll() {
+        final RunningTaskInfo task1 = createFreeformTask();
+        mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, false /* visible */);
+        when(mShellTaskOrganizer.getRunningTaskInfo(task1.taskId)).thenReturn(task1);
+        final RunningTaskInfo task2 = createFreeformTask();
+        mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+        mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
+        mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+        when(mShellTaskOrganizer.getRunningTaskInfo(task2.taskId)).thenReturn(task2);
+
+        mController.showDesktopApps();
+
+        final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
+        // Both tasks should be reordered to top, even if one was already visible.
+        assertThat(wct.getHierarchyOps()).hasSize(2);
+        final HierarchyOp op1 = wct.getHierarchyOps().get(0);
+        assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+        assertThat(op1.getContainer()).isEqualTo(task1.token.asBinder());
+        final HierarchyOp op2 = wct.getHierarchyOps().get(1);
+        assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+        assertThat(op2.getContainer()).isEqualTo(task2.token.asBinder());
     }
 
     @Test
@@ -309,6 +362,12 @@
         assertThat(wct).isNotNull();
     }
 
+    private DesktopModeController createController() {
+        return new DesktopModeController(mContext, mShellInit, mShellController,
+                mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions,
+                mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor());
+    }
+
     private DisplayAreaInfo createMockDisplayArea() {
         DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().mToken,
                 mContext.getDisplayId(), 0);
@@ -355,6 +414,17 @@
         return arg.getValue();
     }
 
+    private WindowContainerTransaction getBringAppsToFrontTransaction() {
+        final ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), arg.capture(), any());
+        } else {
+            verify(mShellTaskOrganizer).applyTransaction(arg.capture());
+        }
+        return arg.getValue();
+    }
+
     private void assertThatBoundsCleared(Change change) {
         assertThat((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) != 0).isTrue();
         assertThat(change.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index aaa5c8a..1e43a59 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -140,6 +140,32 @@
         assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3)
     }
 
+    @Test
+    fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() {
+        repo.addOrMoveFreeformTaskToTop(5)
+        repo.addOrMoveFreeformTaskToTop(6)
+        repo.addOrMoveFreeformTaskToTop(7)
+
+        val tasks = repo.getFreeformTasksInZOrder()
+        assertThat(tasks.size).isEqualTo(3)
+        assertThat(tasks[0]).isEqualTo(7)
+        assertThat(tasks[1]).isEqualTo(6)
+        assertThat(tasks[2]).isEqualTo(5)
+    }
+
+    @Test
+    fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() {
+        repo.addOrMoveFreeformTaskToTop(5)
+        repo.addOrMoveFreeformTaskToTop(6)
+        repo.addOrMoveFreeformTaskToTop(7)
+
+        repo.addOrMoveFreeformTaskToTop(6)
+
+        val tasks = repo.getFreeformTasksInZOrder()
+        assertThat(tasks.size).isEqualTo(3)
+        assertThat(tasks.first()).isEqualTo(6)
+    }
+
     class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
         var activeTaskChangedCalls = 0
         override fun onActiveTasksChanged() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS
new file mode 100644
index 0000000..736d4cf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/OWNERS
@@ -0,0 +1,3 @@
+# WM shell sub-module TV pip owners
+galinap@google.com
+bronger@google.com
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java
index 8b134ed..ad6fced 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java
@@ -54,6 +54,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Supplier;
 
 /** Tests of {@link CaptionWindowDecorViewModel} */
@@ -101,7 +102,7 @@
                 mTaskOrganizer,
                 mDisplayController,
                 mSyncQueue,
-                mDesktopModeController,
+                Optional.of(mDesktopModeController),
                 mCaptionWindowDecorFactory,
                 new MockObjectSupplier<>(mMockInputManagers, () -> mock(InputManager.class)));
         mCaptionWindowDecorViewModel.setEventReceiverFactory(mEventReceiverFactory);
@@ -111,7 +112,7 @@
             .create(any(), any(), any(), any(), any(), any(), any(), any());
 
         when(mInputManager.monitorGestureInput(any(), anyInt())).thenReturn(mInputMonitor);
-        when(mEventReceiverFactory.create(any(), any())).thenReturn(mEventReceiver);
+        when(mEventReceiverFactory.create(any(), any(), any())).thenReturn(mEventReceiver);
         when(mInputMonitor.getInputChannel()).thenReturn(mInputChannel);
     }
 
diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java
new file mode 100644
index 0000000..506128e
--- /dev/null
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location.altitude;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.location.Location;
+
+import com.android.internal.location.altitude.GeoidHeightMap;
+import com.android.internal.location.altitude.S2CellIdUtils;
+import com.android.internal.location.altitude.nano.MapParamsProto;
+import com.android.internal.util.Preconditions;
+
+import java.io.IOException;
+
+/**
+ * Converts altitudes reported above the World Geodetic System 1984 (WGS84) reference ellipsoid
+ * into ones above Mean Sea Level.
+ */
+public final class AltitudeConverter {
+
+    private static final double MAX_ABS_VALID_LATITUDE = 90;
+    private static final double MAX_ABS_VALID_LONGITUDE = 180;
+
+    /** Manages a mapping of geoid heights associated with S2 cells. */
+    private final GeoidHeightMap mGeoidHeightMap = new GeoidHeightMap();
+
+    /**
+     * Creates an instance that manages an independent cache to optimized conversions of locations
+     * in proximity to one another.
+     */
+    public AltitudeConverter() {
+    }
+
+    /**
+     * Throws an {@link IllegalArgumentException} if the {@code location} has an invalid latitude,
+     * longitude, or altitude above WGS84.
+     */
+    private static void validate(@NonNull Location location) {
+        Preconditions.checkArgument(
+                isFiniteAndAtAbsMost(location.getLatitude(), MAX_ABS_VALID_LATITUDE),
+                "Invalid latitude: %f", location.getLatitude());
+        Preconditions.checkArgument(
+                isFiniteAndAtAbsMost(location.getLongitude(), MAX_ABS_VALID_LONGITUDE),
+                "Invalid longitude: %f", location.getLongitude());
+        Preconditions.checkArgument(location.hasAltitude(), "Missing altitude above WGS84");
+        Preconditions.checkArgument(Double.isFinite(location.getAltitude()),
+                "Invalid altitude above WGS84: %f", location.getAltitude());
+    }
+
+    private static boolean isFiniteAndAtAbsMost(double value, double rhs) {
+        return Double.isFinite(value) && Math.abs(value) <= rhs;
+    }
+
+    /**
+     * Returns the four S2 cell IDs for the map square associated with the {@code location}.
+     *
+     * <p>The first map cell contains the location, while the others are located horizontally,
+     * vertically, and diagonally, in that order, with respect to the S2 (i,j) coordinate system. If
+     * the diagonal map cell does not exist (i.e., the location is near an S2 cube vertex), its
+     * corresponding ID is set to zero.
+     */
+    @NonNull
+    private static long[] findMapSquare(@NonNull MapParamsProto params,
+            @NonNull Location location) {
+        long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(),
+                location.getLongitude());
+
+        // (0,0) cell.
+        long s0 = S2CellIdUtils.getParent(s2CellId, params.mapS2Level);
+        long[] edgeNeighbors = new long[4];
+        S2CellIdUtils.getEdgeNeighbors(s0, edgeNeighbors);
+
+        // (1,0) cell.
+        int i1 = S2CellIdUtils.getI(s2CellId) > S2CellIdUtils.getI(s0) ? -1 : 1;
+        long s1 = edgeNeighbors[i1 + 2];
+
+        // (0,1) cell.
+        int i2 = S2CellIdUtils.getJ(s2CellId) > S2CellIdUtils.getJ(s0) ? 1 : -1;
+        long s2 = edgeNeighbors[i2 + 1];
+
+        // (1,1) cell.
+        S2CellIdUtils.getEdgeNeighbors(s1, edgeNeighbors);
+        long s3 = 0;
+        for (int i = 0; i < edgeNeighbors.length; i++) {
+            if (edgeNeighbors[i] == s0) {
+                int i3 = (i + i1 * i2 + edgeNeighbors.length) % edgeNeighbors.length;
+                s3 = edgeNeighbors[i3] == s2 ? 0 : edgeNeighbors[i3];
+                break;
+            }
+        }
+
+        // Reuse edge neighbors' array to avoid an extra allocation.
+        edgeNeighbors[0] = s0;
+        edgeNeighbors[1] = s1;
+        edgeNeighbors[2] = s2;
+        edgeNeighbors[3] = s3;
+        return edgeNeighbors;
+    }
+
+    /**
+     * Adds to {@code location} the bilinearly interpolated Mean Sea Level altitude. In addition, a
+     * Mean Sea Level altitude accuracy is added if the {@code location} has a valid vertical
+     * accuracy; otherwise, does not add a corresponding accuracy.
+     */
+    private static void addMslAltitude(@NonNull MapParamsProto params, @NonNull long[] s2CellIds,
+            @NonNull double[] geoidHeightsMeters, @NonNull Location location) {
+        long s0 = s2CellIds[0];
+        double h0 = geoidHeightsMeters[0];
+        double h1 = geoidHeightsMeters[1];
+        double h2 = geoidHeightsMeters[2];
+        double h3 = s2CellIds[3] == 0 ? h0 : geoidHeightsMeters[3];
+
+        // Bilinear interpolation on an S2 square of size equal to that of a map cell. wi and wj
+        // are the normalized [0,1] weights in the i and j directions, respectively, allowing us to
+        // employ the simplified unit square formulation.
+        long s2CellId = S2CellIdUtils.fromLatLngDegrees(location.getLatitude(),
+                location.getLongitude());
+        double sizeIj = 1 << (S2CellIdUtils.MAX_LEVEL - params.mapS2Level);
+        double wi = Math.abs(S2CellIdUtils.getI(s2CellId) - S2CellIdUtils.getI(s0)) / sizeIj;
+        double wj = Math.abs(S2CellIdUtils.getJ(s2CellId) - S2CellIdUtils.getJ(s0)) / sizeIj;
+        double offsetMeters = h0 + (h1 - h0) * wi + (h2 - h0) * wj + (h3 - h1 - h2 + h0) * wi * wj;
+
+        location.setMslAltitudeMeters(location.getAltitude() - offsetMeters);
+        if (location.hasVerticalAccuracy()) {
+            double verticalAccuracyMeters = location.getVerticalAccuracyMeters();
+            if (Double.isFinite(verticalAccuracyMeters) && verticalAccuracyMeters >= 0) {
+                location.setMslAltitudeAccuracyMeters(
+                        (float) Math.hypot(verticalAccuracyMeters, params.modelRmseMeters));
+            }
+        }
+    }
+
+    /**
+     * Adds a Mean Sea Level altitude to the {@code location}. In addition, adds a Mean Sea Level
+     * altitude accuracy if the {@code location} has a finite and non-negative vertical accuracy;
+     * otherwise, does not add a corresponding accuracy.
+     *
+     * <p>Must be called off the main thread as data may be loaded from raw assets. Throws an
+     * {@link IOException} if an I/O error occurs when loading data.
+     *
+     * <p>Throws an {@link IllegalArgumentException} if the {@code location} has an invalid
+     * latitude, longitude, or altitude above WGS84. Specifically:
+     *
+     * <ul>
+     *     <li>The latitude must be between -90 and 90, both inclusive.
+     *     <li>The longitude must be between -180 and 180, both inclusive.
+     *     <li>The altitude above WGS84 must be finite.
+     * </ul>
+     */
+    @WorkerThread
+    public void addMslAltitude(@NonNull Context context, @NonNull Location location)
+            throws IOException {
+        validate(location);
+        MapParamsProto params = GeoidHeightMap.getParams(context);
+        long[] s2CellIds = findMapSquare(params, location);
+        double[] geoidHeightsMeters = mGeoidHeightMap.readGeoidHeights(params, context, s2CellIds);
+        addMslAltitude(params, s2CellIds, geoidHeightsMeters, location);
+    }
+}
diff --git a/location/java/com/android/internal/location/altitude/GeoidHeightMap.java b/location/java/com/android/internal/location/altitude/GeoidHeightMap.java
new file mode 100644
index 0000000..6430eb4
--- /dev/null
+++ b/location/java/com/android/internal/location/altitude/GeoidHeightMap.java
@@ -0,0 +1,367 @@
+/*
+ * 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.internal.location.altitude;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.LruCache;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.location.altitude.nano.MapParamsProto;
+import com.android.internal.location.altitude.nano.S2TileProto;
+import com.android.internal.util.Preconditions;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * Manages a mapping of geoid heights associated with S2 cells, referred to as MAP CELLS.
+ *
+ * <p>Tiles are used extensively to reduce the number of entries needed to be stored in memory and
+ * on disk. A tile associates geoid heights with all map cells of a common parent at a specified S2
+ * level.
+ *
+ * <p>Since bilinear interpolation considers at most four map cells at a time, at most four tiles
+ * are simultaneously stored in memory. These tiles, referred to as CACHE TILES, are each keyed by
+ * its common parent's S2 cell ID, referred to as a CACHE KEY.
+ *
+ * <p>Absent cache tiles needed for interpolation are constructed from larger tiles stored on disk.
+ * The latter tiles, referred to as DISK TILES, are each keyed by its common parent's S2 cell token,
+ * referred to as a DISK TOKEN.
+ */
+public final class GeoidHeightMap {
+
+    private static final Object sLock = new Object();
+
+    @GuardedBy("sLock")
+    @Nullable
+    private static MapParamsProto sParams;
+
+    /** Defines a cache large enough to hold all cache tiles needed for interpolation. */
+    private final LruCache<Long, S2TileProto> mCacheTiles = new LruCache<>(4);
+
+    /**
+     * Returns the singleton parameter instance for a spherically projected geoid height map and its
+     * corresponding tile management.
+     */
+    @NonNull
+    public static MapParamsProto getParams(@NonNull Context context) throws IOException {
+        synchronized (sLock) {
+            if (sParams == null) {
+                try (InputStream is = context.getApplicationContext().getAssets().open(
+                        "geoid_height_map/map-params.pb")) {
+                    sParams = MapParamsProto.parseFrom(is.readAllBytes());
+                }
+            }
+            return sParams;
+        }
+    }
+
+    private static long getCacheKey(@NonNull MapParamsProto params, long s2CellId) {
+        return S2CellIdUtils.getParent(s2CellId, params.cacheTileS2Level);
+    }
+
+    @NonNull
+    private static String getDiskToken(@NonNull MapParamsProto params, long s2CellId) {
+        return S2CellIdUtils.getToken(
+                S2CellIdUtils.getParent(s2CellId, params.diskTileS2Level));
+    }
+
+    /**
+     * Adds to {@code values} values in the unit interval [0, 1] for the map cells identified by
+     * {@code s2CellIds}. Returns true if values are present for all non-zero IDs; otherwise,
+     * returns false and adds NaNs for absent values.
+     */
+    private static boolean getUnitIntervalValues(@NonNull MapParamsProto params,
+            @NonNull TileFunction tileFunction,
+            @NonNull long[] s2CellIds, @NonNull double[] values) {
+        int len = s2CellIds.length;
+
+        S2TileProto[] tiles = new S2TileProto[len];
+        for (int i = 0; i < len; i++) {
+            if (s2CellIds[i] != 0) {
+                tiles[i] = tileFunction.getTile(s2CellIds[i]);
+            }
+            values[i] = Double.NaN;
+        }
+
+        for (int i = 0; i < len; i++) {
+            if (tiles[i] == null || !Double.isNaN(values[i])) {
+                continue;
+            }
+
+            mergeByteBufferValues(params, s2CellIds, tiles, i, values);
+            mergeByteJpegValues(params, s2CellIds, tiles, i, values);
+            mergeBytePngValues(params, s2CellIds, tiles, i, values);
+        }
+
+        boolean allFound = true;
+        for (int i = 0; i < len; i++) {
+            if (s2CellIds[i] == 0) {
+                continue;
+            }
+            if (Double.isNaN(values[i])) {
+                allFound = false;
+            } else {
+                values[i] = (((int) values[i]) & 0xFF) / 255.0;
+            }
+        }
+        return allFound;
+    }
+
+    @SuppressWarnings("ReferenceEquality")
+    private static void mergeByteBufferValues(@NonNull MapParamsProto params,
+            @NonNull long[] s2CellIds,
+            @NonNull S2TileProto[] tiles,
+            int tileIndex, @NonNull double[] values) {
+        byte[] bytes = tiles[tileIndex].byteBuffer;
+        if (bytes == null || bytes.length == 0) {
+            return;
+        }
+
+        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
+        int tileS2Level = params.mapS2Level - Integer.numberOfTrailingZeros(byteBuffer.limit()) / 2;
+        int numBitsLeftOfTile = 2 * tileS2Level + 3;
+
+        for (int i = tileIndex; i < tiles.length; i++) {
+            if (tiles[i] != tiles[tileIndex]) {
+                continue;
+            }
+
+            long maskedS2CellId = s2CellIds[i] & (-1L >>> numBitsLeftOfTile);
+            int numBitsRightOfMap = 2 * (S2CellIdUtils.MAX_LEVEL - params.mapS2Level) + 1;
+            int bufferIndex = (int) (maskedS2CellId >>> numBitsRightOfMap);
+            values[i] = Double.isNaN(values[i]) ? 0 : values[i];
+            values[i] += ((int) byteBuffer.get(bufferIndex)) & 0xFF;
+        }
+    }
+
+    private static void mergeByteJpegValues(@NonNull MapParamsProto params,
+            @NonNull long[] s2CellIds,
+            @NonNull S2TileProto[] tiles,
+            int tileIndex, @NonNull double[] values) {
+        mergeByteImageValues(params, tiles[tileIndex].byteJpeg, s2CellIds, tiles, tileIndex,
+                values);
+    }
+
+    private static void mergeBytePngValues(@NonNull MapParamsProto params,
+            @NonNull long[] s2CellIds,
+            @NonNull S2TileProto[] tiles,
+            int tileIndex, @NonNull double[] values) {
+        mergeByteImageValues(params, tiles[tileIndex].bytePng, s2CellIds, tiles, tileIndex, values);
+    }
+
+    @SuppressWarnings("ReferenceEquality")
+    private static void mergeByteImageValues(@NonNull MapParamsProto params, @NonNull byte[] bytes,
+            @NonNull long[] s2CellIds,
+            @NonNull S2TileProto[] tiles, int tileIndex, @NonNull double[] values) {
+        if (bytes == null || bytes.length == 0) {
+            return;
+        }
+        Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+        if (bitmap == null) {
+            return;
+        }
+
+        for (int i = tileIndex; i < tiles.length; i++) {
+            if (s2CellIds[i] == 0 || tiles[i] != tiles[tileIndex]) {
+                continue;
+            }
+
+            values[i] = Double.isNaN(values[i]) ? 0 : values[i];
+            values[i] += bitmap.getPixel(getIndexX(params, s2CellIds[i], bitmap.getWidth()),
+                    getIndexY(params, s2CellIds[i], bitmap.getHeight())) & 0xFF;
+        }
+    }
+
+    /** Returns the X index for an S2 cell within an S2 tile image of specified width. */
+    private static int getIndexX(@NonNull MapParamsProto params, long s2CellId, int width) {
+        return getIndexXOrY(params, S2CellIdUtils.getI(s2CellId), width);
+    }
+
+    /** Returns the Y index for an S2 cell within an S2 tile image of specified height. */
+    private static int getIndexY(@NonNull MapParamsProto params, long s2CellId, int height) {
+        return getIndexXOrY(params, S2CellIdUtils.getJ(s2CellId), height);
+    }
+
+    private static int getIndexXOrY(@NonNull MapParamsProto params, int iOrJ, int widthOrHeight) {
+        return (iOrJ >> (S2CellIdUtils.MAX_LEVEL - params.mapS2Level)) % widthOrHeight;
+    }
+
+    /**
+     * Returns the geoid heights in meters associated with the map cells identified by
+     * {@code s2CellIds}. Throws an {@link IOException} if a geoid height cannot be calculated for a
+     * non-zero ID.
+     */
+    @NonNull
+    public double[] readGeoidHeights(@NonNull MapParamsProto params, @NonNull Context context,
+            @NonNull long[] s2CellIds) throws IOException {
+        Preconditions.checkArgument(s2CellIds.length == 4);
+        for (long s2CellId : s2CellIds) {
+            Preconditions.checkArgument(
+                    s2CellId == 0 || S2CellIdUtils.getLevel(s2CellId) == params.mapS2Level);
+        }
+
+        double[] heightsMeters = new double[s2CellIds.length];
+        if (getGeoidHeights(params, mCacheTiles::get, s2CellIds, heightsMeters)) {
+            return heightsMeters;
+        }
+
+        TileFunction loadedTiles = loadFromCacheAndDisk(params, context, s2CellIds);
+        if (getGeoidHeights(params, loadedTiles, s2CellIds, heightsMeters)) {
+            return heightsMeters;
+        }
+        throw new IOException("Unable to calculate geoid heights from raw assets.");
+    }
+
+    /**
+     * Adds to {@code heightsMeters} the geoid heights in meters associated with the map cells
+     * identified by {@code s2CellIds}. Returns true if heights are present for all non-zero IDs;
+     * otherwise, returns false and adds NaNs for absent heights.
+     */
+    private boolean getGeoidHeights(@NonNull MapParamsProto params,
+            @NonNull TileFunction tileFunction, @NonNull long[] s2CellIds,
+            @NonNull double[] heightsMeters) {
+        boolean allFound = getUnitIntervalValues(params, tileFunction, s2CellIds, heightsMeters);
+        for (int i = 0; i < heightsMeters.length; i++) {
+            // NaNs are properly preserved.
+            heightsMeters[i] *= params.modelAMeters;
+            heightsMeters[i] += params.modelBMeters;
+        }
+        return allFound;
+    }
+
+    @NonNull
+    private TileFunction loadFromCacheAndDisk(@NonNull MapParamsProto params,
+            @NonNull Context context, @NonNull long[] s2CellIds) throws IOException {
+        int len = s2CellIds.length;
+
+        // Enable batch loading by finding all cache keys upfront.
+        long[] cacheKeys = new long[len];
+        for (int i = 0; i < len; i++) {
+            if (s2CellIds[i] == 0) {
+                continue;
+            }
+            cacheKeys[i] = getCacheKey(params, s2CellIds[i]);
+        }
+
+        // Attempt to load tiles from cache.
+        S2TileProto[] loadedTiles = new S2TileProto[len];
+        String[] diskTokens = new String[len];
+        for (int i = 0; i < len; i++) {
+            if (s2CellIds[i] == 0 || diskTokens[i] != null) {
+                continue;
+            }
+            loadedTiles[i] = mCacheTiles.get(cacheKeys[i]);
+            diskTokens[i] = getDiskToken(params, cacheKeys[i]);
+
+            // Batch across common cache key.
+            for (int j = i + 1; j < len; j++) {
+                if (cacheKeys[j] == cacheKeys[i]) {
+                    loadedTiles[j] = loadedTiles[i];
+                    diskTokens[j] = diskTokens[i];
+                }
+            }
+        }
+
+        // Attempt to load tiles from disk.
+        for (int i = 0; i < len; i++) {
+            if (s2CellIds[i] == 0 || loadedTiles[i] != null) {
+                continue;
+            }
+
+            S2TileProto tile;
+            try (InputStream is = context.getApplicationContext().getAssets().open(
+                    "geoid_height_map/tile-" + diskTokens[i] + ".pb")) {
+                tile = S2TileProto.parseFrom(is.readAllBytes());
+            }
+            mergeFromDiskTile(params, tile, cacheKeys, diskTokens, i, loadedTiles);
+        }
+
+        return s2CellId -> {
+            if (s2CellId == 0) {
+                return null;
+            }
+            long cacheKey = getCacheKey(params, s2CellId);
+            for (int i = 0; i < cacheKeys.length; i++) {
+                if (cacheKeys[i] == cacheKey) {
+                    return loadedTiles[i];
+                }
+            }
+            return null;
+        };
+    }
+
+    private void mergeFromDiskTile(@NonNull MapParamsProto params, @NonNull S2TileProto diskTile,
+            @NonNull long[] cacheKeys, @NonNull String[] diskTokens, int diskTokenIndex,
+            @NonNull S2TileProto[] loadedTiles) throws IOException {
+        int len = cacheKeys.length;
+        int numMapCellsPerCacheTile = 1 << (2 * (params.mapS2Level - params.cacheTileS2Level));
+
+        // Reusable arrays.
+        long[] s2CellIds = new long[numMapCellsPerCacheTile];
+        double[] values = new double[numMapCellsPerCacheTile];
+
+        // Each cache key identifies a different sub-tile of the disk tile.
+        TileFunction diskTileFunction = s2CellId -> diskTile;
+        for (int i = diskTokenIndex; i < len; i++) {
+            if (!Objects.equals(diskTokens[i], diskTokens[diskTokenIndex])
+                    || loadedTiles[i] != null) {
+                continue;
+            }
+
+            // Find all map cells within the current cache tile.
+            long s2CellId = S2CellIdUtils.getTraversalStart(cacheKeys[i], params.mapS2Level);
+            for (int j = 0; j < numMapCellsPerCacheTile; j++) {
+                s2CellIds[j] = s2CellId;
+                s2CellId = S2CellIdUtils.getTraversalNext(s2CellId);
+            }
+
+            if (!getUnitIntervalValues(params, diskTileFunction, s2CellIds, values)) {
+                throw new IOException("Corrupted disk tile of disk token: " + diskTokens[i]);
+            }
+
+            loadedTiles[i] = new S2TileProto();
+            loadedTiles[i].byteBuffer = new byte[numMapCellsPerCacheTile];
+            for (int j = 0; j < numMapCellsPerCacheTile; j++) {
+                loadedTiles[i].byteBuffer[j] = (byte) Math.round(values[j] * 0xFF);
+            }
+
+            // Batch across common cache key.
+            for (int j = i + 1; j < len; j++) {
+                if (cacheKeys[j] == cacheKeys[i]) {
+                    loadedTiles[j] = loadedTiles[i];
+                }
+            }
+
+            // Side load into tile cache.
+            mCacheTiles.put(cacheKeys[i], loadedTiles[i]);
+        }
+    }
+
+    /** Defines a function-like object to retrieve tiles for map cells. */
+    private interface TileFunction {
+
+        @Nullable
+        S2TileProto getTile(long s2CellId);
+    }
+}
diff --git a/location/java/com/android/internal/location/altitude/S2CellIdUtils.java b/location/java/com/android/internal/location/altitude/S2CellIdUtils.java
new file mode 100644
index 0000000..5f11387
--- /dev/null
+++ b/location/java/com/android/internal/location/altitude/S2CellIdUtils.java
@@ -0,0 +1,653 @@
+/*
+ * 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.internal.location.altitude;
+
+import android.annotation.NonNull;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Provides lightweight S2 cell ID utilities without traditional geometry dependencies.
+ *
+ * <p>See <a href="https://s2geometry.io/">the S2 Geometry Library website</a> for more details.
+ */
+public final class S2CellIdUtils {
+
+    /** The level of all leaf S2 cells. */
+    public static final int MAX_LEVEL = 30;
+
+    private static final int MAX_SIZE = 1 << MAX_LEVEL;
+    private static final double ONE_OVER_MAX_SIZE = 1.0 / MAX_SIZE;
+    private static final int NUM_FACES = 6;
+    private static final int POS_BITS = 2 * MAX_LEVEL + 1;
+    private static final int SWAP_MASK = 0x1;
+    private static final int LOOKUP_BITS = 4;
+    private static final int LOOKUP_MASK = (1 << LOOKUP_BITS) - 1;
+    private static final int INVERT_MASK = 0x2;
+    private static final int LEAF_MASK = 0x1;
+    private static final int[] LOOKUP_POS = new int[1 << (2 * LOOKUP_BITS + 2)];
+    private static final int[] LOOKUP_IJ = new int[1 << (2 * LOOKUP_BITS + 2)];
+    private static final int[] POS_TO_ORIENTATION = {SWAP_MASK, 0, 0, INVERT_MASK + SWAP_MASK};
+    private static final int[][] POS_TO_IJ =
+            {{0, 1, 3, 2}, {0, 2, 3, 1}, {3, 2, 0, 1}, {3, 1, 0, 2}};
+    private static final double UV_LIMIT = calculateUvLimit();
+    private static final UvTransform[] UV_TRANSFORMS = createUvTransforms();
+    private static final XyzTransform[] XYZ_TRANSFORMS = createXyzTransforms();
+
+    // Used to encode (i, j, o) coordinates into primitive longs.
+    private static final int I_SHIFT = 33;
+    private static final int J_SHIFT = 2;
+    private static final long J_MASK = (1L << 31) - 1;
+
+    static {
+        initLookupCells();
+    }
+
+    /** Prevents instantiation. */
+    private S2CellIdUtils() {
+    }
+
+    /**
+     * Returns the leaf S2 cell ID for the specified latitude and longitude, both measured in
+     * degrees.
+     */
+    public static long fromLatLngDegrees(double latDegrees, double lngDegrees) {
+        return fromLatLngRadians(Math.toRadians(latDegrees), Math.toRadians(lngDegrees));
+    }
+
+    /**
+     * Returns the ID of the parent of the specified S2 cell at the specified parent level.
+     * Behavior is undefined for invalid S2 cell IDs or parent levels not in
+     * [0, {@code getLevel(s2CellId)}[.
+     */
+    public static long getParent(long s2CellId, int level) {
+        long newLsb = getLowestOnBitForLevel(level);
+        return (s2CellId & -newLsb) | newLsb;
+    }
+
+    /**
+     * Inserts into {@code neighbors} the four S2 cell IDs corresponding to the neighboring
+     * cells adjacent across the specified cell's four edges. This array must be of minimum
+     * length four, and elements at the tail end of the array not corresponding to a neighbor
+     * are set to zero. A reference to this array is returned.
+     *
+     * <p>Inserts in the order of down, right, up, and left directions, in that order. All
+     * neighbors are guaranteed to be distinct.
+     */
+    public static void getEdgeNeighbors(long s2CellId, @NonNull long[] neighbors) {
+        int level = getLevel(s2CellId);
+        int size = levelToSizeIj(level);
+        int face = getFace(s2CellId);
+        long ijo = toIjo(s2CellId);
+        int i = ijoToI(ijo);
+        int j = ijoToJ(ijo);
+
+        int iPlusSize = i + size;
+        int iMinusSize = i - size;
+        int jPlusSize = j + size;
+        int jMinusSize = j - size;
+        boolean iPlusSizeLtMax = iPlusSize < MAX_SIZE;
+        boolean iMinusSizeGteZero = iMinusSize >= 0;
+        boolean jPlusSizeLtMax = jPlusSize < MAX_SIZE;
+        boolean jMinusSizeGteZero = jMinusSize >= 0;
+
+        int index = 0;
+        // Down direction.
+        neighbors[index++] = getParent(fromFijSame(face, i, jMinusSize, jMinusSizeGteZero),
+                level);
+        // Right direction.
+        neighbors[index++] = getParent(fromFijSame(face, iPlusSize, j, iPlusSizeLtMax), level);
+        // Up direction.
+        neighbors[index++] = getParent(fromFijSame(face, i, jPlusSize, jPlusSizeLtMax), level);
+        // Left direction.
+        neighbors[index++] = getParent(fromFijSame(face, iMinusSize, j, iMinusSizeGteZero),
+                level);
+
+        // Pad end of neighbor array with zeros.
+        Arrays.fill(neighbors, index, neighbors.length, 0);
+    }
+
+    /** Returns the "i" coordinate for the specified S2 cell. */
+    public static int getI(long s2CellId) {
+        return ijoToI(toIjo(s2CellId));
+    }
+
+    /** Returns the "j" coordinate for the specified S2 cell. */
+    public static int getJ(long s2CellId) {
+        return ijoToJ(toIjo(s2CellId));
+    }
+
+    /**
+     * Returns the leaf S2 cell ID for the specified latitude and longitude, both measured in
+     * radians.
+     */
+    private static long fromLatLngRadians(double latRadians, double lngRadians) {
+        double cosLat = Math.cos(latRadians);
+        double x = Math.cos(lngRadians) * cosLat;
+        double y = Math.sin(lngRadians) * cosLat;
+        double z = Math.sin(latRadians);
+        return fromXyz(x, y, z);
+    }
+
+    /**
+     * Returns the level of the specified S2 cell. The returned level is in [0, 30] for valid
+     * S2 cell IDs. Behavior is undefined for invalid S2 cell IDs.
+     */
+    static int getLevel(long s2CellId) {
+        if (isLeaf(s2CellId)) {
+            return MAX_LEVEL;
+        }
+        return MAX_LEVEL - (Long.numberOfTrailingZeros(s2CellId) >> 1);
+    }
+
+    /** Returns the lowest-numbered bit that is on for the specified S2 cell. */
+    static long getLowestOnBit(long s2CellId) {
+        return s2CellId & -s2CellId;
+    }
+
+    /** Returns the lowest-numbered bit that is on for any S2 cell on the specified level. */
+    static long getLowestOnBitForLevel(int level) {
+        return 1L << (2 * (MAX_LEVEL - level));
+    }
+
+    /**
+     * Returns the ID of the first S2 cell in a traversal of the children S2 cells at the specified
+     * level, in Hilbert curve order.
+     */
+    static long getTraversalStart(long s2CellId, int level) {
+        return s2CellId - getLowestOnBit(s2CellId) + getLowestOnBitForLevel(level);
+    }
+
+    /** Returns the ID of the next S2 cell at the same level along the Hilbert curve. */
+    static long getTraversalNext(long s2CellId) {
+        return s2CellId + (getLowestOnBit(s2CellId) << 1);
+    }
+
+    /**
+     * Encodes the S2 cell id to compact text strings suitable for display or indexing. Cells at
+     * lower levels (i.e., larger cells) are encoded into fewer characters.
+     */
+    @NonNull
+    static String getToken(long s2CellId) {
+        if (s2CellId == 0) {
+            return "X";
+        }
+
+        // Convert to a hex string with as many digits as necessary.
+        String hex = Long.toHexString(s2CellId).toLowerCase(Locale.US);
+        // Prefix 0s to get a length 16 string.
+        String padded = padStart(hex);
+        // Trim zeroes off the end.
+        return padded.replaceAll("0*$", "");
+    }
+
+    private static String padStart(String string) {
+        if (string.length() >= 16) {
+            return string;
+        }
+        return "0".repeat(16 - string.length()) + string;
+    }
+
+    /** Returns the leaf S2 cell ID of the specified (x, y, z) coordinate. */
+    private static long fromXyz(double x, double y, double z) {
+        int face = xyzToFace(x, y, z);
+        UvTransform uvTransform = UV_TRANSFORMS[face];
+        double u = uvTransform.xyzToU(x, y, z);
+        double v = uvTransform.xyzToV(x, y, z);
+        return fromFuv(face, u, v);
+    }
+
+    /** Returns the leaf S2 cell ID of the specified (face, u, v) coordinate. */
+    private static long fromFuv(int face, double u, double v) {
+        int i = uToI(u);
+        int j = vToJ(v);
+        return fromFij(face, i, j);
+    }
+
+    /** Returns the leaf S2 cell ID of the specified (face, i, j) coordinate. */
+    private static long fromFij(int face, int i, int j) {
+        int bits = (face & SWAP_MASK);
+        // Update most significant bits.
+        long msb = ((long) face) << (POS_BITS - 33);
+        for (int k = 7; k >= 4; --k) {
+            bits = lookupBits(i, j, k, bits);
+            msb = updateBits(msb, k, bits);
+            bits = maskBits(bits);
+        }
+        // Update least significant bits.
+        long lsb = 0;
+        for (int k = 3; k >= 0; --k) {
+            bits = lookupBits(i, j, k, bits);
+            lsb = updateBits(lsb, k, bits);
+            bits = maskBits(bits);
+        }
+        return (((msb << 32) + lsb) << 1) + 1;
+    }
+
+    private static long fromFijWrap(int face, int i, int j) {
+        double u = iToU(i);
+        double v = jToV(j);
+
+        XyzTransform xyzTransform = XYZ_TRANSFORMS[face];
+        double x = xyzTransform.uvToX(u, v);
+        double y = xyzTransform.uvToY(u, v);
+        double z = xyzTransform.uvToZ(u, v);
+
+        int newFace = xyzToFace(x, y, z);
+        UvTransform uvTransform = UV_TRANSFORMS[newFace];
+        double newU = uvTransform.xyzToU(x, y, z);
+        double newV = uvTransform.xyzToV(x, y, z);
+
+        int newI = uShiftIntoI(newU);
+        int newJ = vShiftIntoJ(newV);
+        return fromFij(newFace, newI, newJ);
+    }
+
+    private static long fromFijSame(int face, int i, int j, boolean isSameFace) {
+        if (isSameFace) {
+            return fromFij(face, i, j);
+        }
+        return fromFijWrap(face, i, j);
+    }
+
+    /**
+     * Returns the face associated with the specified (x, y, z) coordinate. For a coordinate
+     * on a face boundary, the returned face is arbitrary but repeatable.
+     */
+    private static int xyzToFace(double x, double y, double z) {
+        double absX = Math.abs(x);
+        double absY = Math.abs(y);
+        double absZ = Math.abs(z);
+        if (absX > absY) {
+            if (absX > absZ) {
+                return (x < 0) ? 3 : 0;
+            }
+            return (z < 0) ? 5 : 2;
+        }
+        if (absY > absZ) {
+            return (y < 0) ? 4 : 1;
+        }
+        return (z < 0) ? 5 : 2;
+    }
+
+    private static int uToI(double u) {
+        double s;
+        if (u >= 0) {
+            s = 0.5 * Math.sqrt(1 + 3 * u);
+        } else {
+            s = 1 - 0.5 * Math.sqrt(1 - 3 * u);
+        }
+        return Math.max(0, Math.min(MAX_SIZE - 1, (int) Math.round(MAX_SIZE * s - 0.5)));
+    }
+
+    private static int vToJ(double v) {
+        // Same calculation as uToI.
+        return uToI(v);
+    }
+
+    private static int lookupBits(int i, int j, int k, int bits) {
+        bits += ((i >> (k * LOOKUP_BITS)) & LOOKUP_MASK) << (LOOKUP_BITS + 2);
+        bits += ((j >> (k * LOOKUP_BITS)) & LOOKUP_MASK) << 2;
+        return LOOKUP_POS[bits];
+    }
+
+    private static long updateBits(long sb, int k, int bits) {
+        return sb | ((((long) bits) >> 2) << ((k & 0x3) * 2 * LOOKUP_BITS));
+    }
+
+    private static int maskBits(int bits) {
+        return bits & (SWAP_MASK | INVERT_MASK);
+    }
+
+    private static int getFace(long s2CellId) {
+        return (int) (s2CellId >>> POS_BITS);
+    }
+
+    private static boolean isLeaf(long s2CellId) {
+        return ((int) s2CellId & LEAF_MASK) != 0;
+    }
+
+    private static double iToU(int i) {
+        int satI = Math.max(-1, Math.min(MAX_SIZE, i));
+        return Math.max(
+                -UV_LIMIT,
+                Math.min(UV_LIMIT, ONE_OVER_MAX_SIZE * ((satI << 1) + 1 - MAX_SIZE)));
+    }
+
+    private static double jToV(int j) {
+        // Same calculation as iToU.
+        return iToU(j);
+    }
+
+    private static long toIjo(long s2CellId) {
+        int face = getFace(s2CellId);
+        int bits = face & SWAP_MASK;
+        int i = 0;
+        int j = 0;
+        for (int k = 7; k >= 0; --k) {
+            int nbits = (k == 7) ? (MAX_LEVEL - 7 * LOOKUP_BITS) : LOOKUP_BITS;
+            bits += ((int) (s2CellId >>> (k * 2 * LOOKUP_BITS + 1)) & ((1 << (2 * nbits))
+                    - 1)) << 2;
+            bits = LOOKUP_IJ[bits];
+            i += (bits >> (LOOKUP_BITS + 2)) << (k * LOOKUP_BITS);
+            j += ((bits >> 2) & ((1 << LOOKUP_BITS) - 1)) << (k * LOOKUP_BITS);
+            bits &= (SWAP_MASK | INVERT_MASK);
+        }
+        int orientation =
+                ((getLowestOnBit(s2CellId) & 0x1111111111111110L) != 0) ? (bits ^ SWAP_MASK)
+                        : bits;
+        return (((long) i) << I_SHIFT) | (((long) j) << J_SHIFT) | orientation;
+    }
+
+    private static int ijoToI(long ijo) {
+        return (int) (ijo >>> I_SHIFT);
+    }
+
+    private static int ijoToJ(long ijo) {
+        return (int) ((ijo >>> J_SHIFT) & J_MASK);
+    }
+
+    private static int uShiftIntoI(double u) {
+        double s = 0.5 * (u + 1);
+        return Math.max(0, Math.min(MAX_SIZE - 1, (int) Math.round(MAX_SIZE * s - 0.5)));
+    }
+
+    private static int vShiftIntoJ(double v) {
+        // Same calculation as uShiftIntoI.
+        return uShiftIntoI(v);
+    }
+
+    private static int levelToSizeIj(int level) {
+        return 1 << (MAX_LEVEL - level);
+    }
+
+    private static void initLookupCells() {
+        initLookupCell(0, 0, 0, 0, 0, 0);
+        initLookupCell(0, 0, 0, SWAP_MASK, 0, SWAP_MASK);
+        initLookupCell(0, 0, 0, INVERT_MASK, 0, INVERT_MASK);
+        initLookupCell(0, 0, 0, SWAP_MASK | INVERT_MASK, 0, SWAP_MASK | INVERT_MASK);
+    }
+
+    private static void initLookupCell(
+            int level, int i, int j, int origOrientation, int pos, int orientation) {
+        if (level == LOOKUP_BITS) {
+            int ij = (i << LOOKUP_BITS) + j;
+            LOOKUP_POS[(ij << 2) + origOrientation] = (pos << 2) + orientation;
+            LOOKUP_IJ[(pos << 2) + origOrientation] = (ij << 2) + orientation;
+        } else {
+            level++;
+            i <<= 1;
+            j <<= 1;
+            pos <<= 2;
+            for (int subPos = 0; subPos < 4; subPos++) {
+                int ij = POS_TO_IJ[orientation][subPos];
+                int orientationMask = POS_TO_ORIENTATION[subPos];
+                initLookupCell(
+                        level,
+                        i + (ij >>> 1),
+                        j + (ij & 0x1),
+                        origOrientation,
+                        pos + subPos,
+                        orientation ^ orientationMask);
+            }
+        }
+    }
+
+    private static double calculateUvLimit() {
+        double machEps = 1.0;
+        do {
+            machEps /= 2.0f;
+        } while ((1.0 + (machEps / 2.0)) != 1.0);
+        return 1.0 + machEps;
+    }
+
+    @NonNull
+    private static UvTransform[] createUvTransforms() {
+        UvTransform[] uvTransforms = new UvTransform[NUM_FACES];
+        uvTransforms[0] =
+                new UvTransform() {
+
+                    @Override
+                    public double xyzToU(double x, double y, double z) {
+                        return y / x;
+                    }
+
+                    @Override
+                    public double xyzToV(double x, double y, double z) {
+                        return z / x;
+                    }
+                };
+        uvTransforms[1] =
+                new UvTransform() {
+
+                    @Override
+                    public double xyzToU(double x, double y, double z) {
+                        return -x / y;
+                    }
+
+                    @Override
+                    public double xyzToV(double x, double y, double z) {
+                        return z / y;
+                    }
+                };
+        uvTransforms[2] =
+                new UvTransform() {
+
+                    @Override
+                    public double xyzToU(double x, double y, double z) {
+                        return -x / z;
+                    }
+
+                    @Override
+                    public double xyzToV(double x, double y, double z) {
+                        return -y / z;
+                    }
+                };
+        uvTransforms[3] =
+                new UvTransform() {
+
+                    @Override
+                    public double xyzToU(double x, double y, double z) {
+                        return z / x;
+                    }
+
+                    @Override
+                    public double xyzToV(double x, double y, double z) {
+                        return y / x;
+                    }
+                };
+        uvTransforms[4] =
+                new UvTransform() {
+
+                    @Override
+                    public double xyzToU(double x, double y, double z) {
+                        return z / y;
+                    }
+
+                    @Override
+                    public double xyzToV(double x, double y, double z) {
+                        return -x / y;
+                    }
+                };
+        uvTransforms[5] =
+                new UvTransform() {
+
+                    @Override
+                    public double xyzToU(double x, double y, double z) {
+                        return -y / z;
+                    }
+
+                    @Override
+                    public double xyzToV(double x, double y, double z) {
+                        return -x / z;
+                    }
+                };
+        return uvTransforms;
+    }
+
+    @NonNull
+    private static XyzTransform[] createXyzTransforms() {
+        XyzTransform[] xyzTransforms = new XyzTransform[NUM_FACES];
+        xyzTransforms[0] =
+                new XyzTransform() {
+
+                    @Override
+                    public double uvToX(double u, double v) {
+                        return 1;
+                    }
+
+                    @Override
+                    public double uvToY(double u, double v) {
+                        return u;
+                    }
+
+                    @Override
+                    public double uvToZ(double u, double v) {
+                        return v;
+                    }
+                };
+        xyzTransforms[1] =
+                new XyzTransform() {
+
+                    @Override
+                    public double uvToX(double u, double v) {
+                        return -u;
+                    }
+
+                    @Override
+                    public double uvToY(double u, double v) {
+                        return 1;
+                    }
+
+                    @Override
+                    public double uvToZ(double u, double v) {
+                        return v;
+                    }
+                };
+        xyzTransforms[2] =
+                new XyzTransform() {
+
+                    @Override
+                    public double uvToX(double u, double v) {
+                        return -u;
+                    }
+
+                    @Override
+                    public double uvToY(double u, double v) {
+                        return -v;
+                    }
+
+                    @Override
+                    public double uvToZ(double u, double v) {
+                        return 1;
+                    }
+                };
+        xyzTransforms[3] =
+                new XyzTransform() {
+
+                    @Override
+                    public double uvToX(double u, double v) {
+                        return -1;
+                    }
+
+                    @Override
+                    public double uvToY(double u, double v) {
+                        return -v;
+                    }
+
+                    @Override
+                    public double uvToZ(double u, double v) {
+                        return -u;
+                    }
+                };
+        xyzTransforms[4] =
+                new XyzTransform() {
+
+                    @Override
+                    public double uvToX(double u, double v) {
+                        return v;
+                    }
+
+                    @Override
+                    public double uvToY(double u, double v) {
+                        return -1;
+                    }
+
+                    @Override
+                    public double uvToZ(double u, double v) {
+                        return -u;
+                    }
+                };
+        xyzTransforms[5] =
+                new XyzTransform() {
+
+                    @Override
+                    public double uvToX(double u, double v) {
+                        return v;
+                    }
+
+                    @Override
+                    public double uvToY(double u, double v) {
+                        return u;
+                    }
+
+                    @Override
+                    public double uvToZ(double u, double v) {
+                        return -1;
+                    }
+                };
+        return xyzTransforms;
+    }
+
+    /**
+     * Transform from (x, y, z) coordinates to (u, v) coordinates, indexed by face. For a
+     * (x, y, z) coordinate within a face, each element of the resulting (u, v) coordinate
+     * should lie in the inclusive range [-1, 1], with the face center having a (u, v)
+     * coordinate equal to (0, 0).
+     */
+    private interface UvTransform {
+
+        /**
+         * Returns for the specified (x, y, z) coordinate the corresponding u-coordinate
+         * (which may lie outside the range [-1, 1]).
+         */
+        double xyzToU(double x, double y, double z);
+
+        /**
+         * Returns for the specified (x, y, z) coordinate the corresponding v-coordinate
+         * (which may lie outside the range [-1, 1]).
+         */
+        double xyzToV(double x, double y, double z);
+    }
+
+    /**
+     * Transform from (u, v) coordinates to (x, y, z) coordinates, indexed by face. The
+     * resulting vectors are not necessarily of unit length.
+     */
+    private interface XyzTransform {
+
+        /** Returns for the specified (u, v) coordinate the corresponding x-coordinate. */
+        double uvToX(double u, double v);
+
+        /** Returns for the specified (u, v) coordinate the corresponding y-coordinate. */
+        double uvToY(double u, double v);
+
+        /** Returns for the specified (u, v) coordinate the corresponding z-coordinate. */
+        double uvToZ(double u, double v);
+    }
+}
diff --git a/media/java/android/media/AudioHalVersionInfo.aidl b/media/java/android/media/AudioHalVersionInfo.aidl
new file mode 100644
index 0000000..a83f8c8
--- /dev/null
+++ b/media/java/android/media/AudioHalVersionInfo.aidl
@@ -0,0 +1,18 @@
+/* Copyright 2022, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.media;
+
+parcelable AudioHalVersionInfo;
diff --git a/media/java/android/media/AudioHalVersionInfo.java b/media/java/android/media/AudioHalVersionInfo.java
new file mode 100644
index 0000000..985a758
--- /dev/null
+++ b/media/java/android/media/AudioHalVersionInfo.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+
+/**
+ * Defines the audio HAL version.
+ *
+ * @hide
+ */
+@TestApi
+public final class AudioHalVersionInfo implements Parcelable, Comparable<AudioHalVersionInfo> {
+    /**
+     * Indicate the audio HAL is implemented with HIDL (HAL interface definition language).
+     *
+     * @see <a href="https://source.android.com/docs/core/architecture/hidl/">HIDL</a>
+     *     <p>The value of AUDIO_HAL_TYPE_HIDL should match the value of {@link
+     *     android.media.AudioHalVersion.Type#HIDL}.
+     */
+    public static final int AUDIO_HAL_TYPE_HIDL = 0;
+
+    /**
+     * Indicate the audio HAL is implemented with AIDL (Android Interface Definition Language).
+     *
+     * @see <a href="https://source.android.com/docs/core/architecture/aidl/">AIDL</a>
+     *     <p>The value of AUDIO_HAL_TYPE_AIDL should match the value of {@link
+     *     android.media.AudioHalVersion.Type#AIDL}.
+     */
+    public static final int AUDIO_HAL_TYPE_AIDL = 1;
+
+    /** @hide */
+    @IntDef(
+            flag = false,
+            prefix = "AUDIO_HAL_TYPE_",
+            value = {AUDIO_HAL_TYPE_HIDL, AUDIO_HAL_TYPE_AIDL})
+    public @interface AudioHalType {}
+
+    /** AudioHalVersionInfo object of all valid Audio HAL versions. */
+    public static final @NonNull AudioHalVersionInfo AIDL_1_0 =
+            new AudioHalVersionInfo(AUDIO_HAL_TYPE_AIDL, 1 /* major */, 0 /* minor */);
+
+    public static final @NonNull AudioHalVersionInfo HIDL_7_1 =
+            new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 7 /* major */, 1 /* minor */);
+    public static final @NonNull AudioHalVersionInfo HIDL_7_0 =
+            new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 7 /* major */, 0 /* minor */);
+    public static final @NonNull AudioHalVersionInfo HIDL_6_0 =
+            new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 6 /* major */, 0 /* minor */);
+    public static final @NonNull AudioHalVersionInfo HIDL_5_0 =
+            new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 5 /* major */, 0 /* minor */);
+    public static final @NonNull AudioHalVersionInfo HIDL_4_0 =
+            new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 4 /* major */, 0 /* minor */);
+    public static final @NonNull AudioHalVersionInfo HIDL_2_0 =
+            new AudioHalVersionInfo(AUDIO_HAL_TYPE_HIDL, 2 /* major */, 0 /* minor */);
+
+    /**
+     * List of all valid Audio HAL versions. This list need to be in sync with sAudioHALVersions
+     * defined in frameworks/av/media/libaudiohal/FactoryHalHidl.cpp.
+     */
+    // TODO: add AIDL_1_0 with sAudioHALVersions.
+    public static final @NonNull List<AudioHalVersionInfo> VERSIONS =
+            List.of(HIDL_7_1, HIDL_7_0, HIDL_6_0, HIDL_5_0, HIDL_4_0);
+
+    private static final String TAG = "AudioHalVersionInfo";
+    private AudioHalVersion mHalVersion = new AudioHalVersion();
+
+    public @AudioHalType int getHalType() {
+        return mHalVersion.type;
+    }
+
+    public int getMajorVersion() {
+        return mHalVersion.major;
+    }
+
+    public int getMinorVersion() {
+        return mHalVersion.minor;
+    }
+
+    /** String representative of AudioHalVersion.Type */
+    private static @NonNull String typeToString(@AudioHalType int type) {
+        if (type == AudioHalVersion.Type.HIDL) {
+            return "HIDL";
+        } else if (type == AudioHalVersion.Type.AIDL) {
+            return "AIDL";
+        } else {
+            return "INVALID";
+        }
+    }
+
+    /** String representative of type, major and minor */
+    private static @NonNull String toString(@AudioHalType int type, int major, int minor) {
+        return typeToString(type) + ":" + Integer.toString(major) + "." + Integer.toString(minor);
+    }
+
+    private AudioHalVersionInfo(@AudioHalType int type, int major, int minor) {
+        mHalVersion.type = type;
+        mHalVersion.major = major;
+        mHalVersion.minor = minor;
+    }
+
+    private AudioHalVersionInfo(Parcel in) {
+        mHalVersion = in.readTypedObject(AudioHalVersion.CREATOR);
+    }
+
+    /** String representative of this (AudioHalVersionInfo) object */
+    @Override
+    public String toString() {
+        return toString(mHalVersion.type, mHalVersion.major, mHalVersion.minor);
+    }
+
+    /**
+     * Compare two HAL versions by comparing their index in VERSIONS.
+     *
+     * <p>Normally all AudioHalVersionInfo object to compare should exist in the VERSIONS list. If
+     * both candidates exist in the VERSIONS list, smaller index means newer. Any candidate not
+     * exist in the VERSIONS list will be considered to be oldest version.
+     *
+     * @return 0 if the HAL version is the same as the other HAL version. Positive if the HAL
+     *     version is newer than the other HAL version. Negative if the HAL version is older than
+     *     the other version.
+     */
+    @Override
+    public int compareTo(@NonNull AudioHalVersionInfo other) {
+        int indexOther = VERSIONS.indexOf(other);
+        int indexThis = VERSIONS.indexOf(this);
+        if (indexThis < 0 || indexOther < 0) {
+            return indexThis - indexOther;
+        }
+        return indexOther - indexThis;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull android.os.Parcel out, int flag) {
+        out.writeTypedObject(mHalVersion, flag);
+    }
+
+    public static final @NonNull Parcelable.Creator<AudioHalVersionInfo> CREATOR =
+            new Parcelable.Creator<AudioHalVersionInfo>() {
+                @Override
+                public AudioHalVersionInfo createFromParcel(@NonNull Parcel in) {
+                    return new AudioHalVersionInfo(in);
+                }
+
+                @Override
+                public AudioHalVersionInfo[] newArray(int size) {
+                    return new AudioHalVersionInfo[size];
+                }
+            };
+}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 9c5313a..ae0d45f 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -8502,13 +8502,14 @@
     }
 
     /**
-     * Returns the audio HAL version in the form MAJOR.MINOR. If there is no audio HAL found, null
-     * will be returned.
+     * Returns an {@link AudioHalVersionInfo} indicating the Audio Hal Version. If there is no audio
+     * HAL found, null will be returned.
      *
+     * @return @see @link #AudioHalVersionInfo The version of Audio HAL.
      * @hide
      */
     @TestApi
-    public static @Nullable String getHalVersion() {
+    public static @Nullable AudioHalVersionInfo getHalVersion() {
         try {
             return getService().getHalVersion();
         } catch (RemoteException e) {
diff --git a/media/java/android/media/AudioPresentation.java b/media/java/android/media/AudioPresentation.java
index 47358be..05f3c5a 100644
--- a/media/java/android/media/AudioPresentation.java
+++ b/media/java/android/media/AudioPresentation.java
@@ -54,7 +54,11 @@
     private final int mProgramId;
     private final ULocale mLanguage;
 
-    /** @hide */
+    /**
+     * The ContentClassifier int definitions represent the AudioPresentation content
+     * classifier (as per TS 103 190-1 v1.2.1 4.3.3.8.1)
+     * @hide
+     */
     @IntDef(
         value = {
         CONTENT_UNKNOWN,
@@ -67,11 +71,6 @@
         CONTENT_EMERGENCY,
         CONTENT_VOICEOVER,
     })
-
-    /**
-     * The ContentClassifier int definitions represent the AudioPresentation content
-     * classifier (as per TS 103 190-1 v1.2.1 4.3.3.8.1)
-    */
     @Retention(RetentionPolicy.SOURCE)
     public @interface ContentClassifier {}
 
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
old mode 100755
new mode 100644
index 2e766d5..ee453a4
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -22,6 +22,7 @@
 import android.media.AudioDeviceAttributes;
 import android.media.AudioFormat;
 import android.media.AudioFocusInfo;
+import android.media.AudioHalVersionInfo;
 import android.media.AudioPlaybackConfiguration;
 import android.media.AudioRecordingConfiguration;
 import android.media.AudioRoutesInfo;
@@ -571,5 +572,5 @@
             in AudioDeviceAttributes device, in List<VolumeInfo> volumes,
             boolean handlesvolumeAdjustment);
 
-    String getHalVersion();
+    AudioHalVersionInfo getHalVersion();
 }
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index bf30c50..30d90a8 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -4106,6 +4106,22 @@
         public static final int AV1Level72      = 0x400000;
         public static final int AV1Level73      = 0x800000;
 
+        /** DTS codec profile for DTS HRA. */
+        @SuppressLint("AllUpper")
+        public static final int DTS_HDProfileHRA = 0x1;
+        /** DTS codec profile for DTS Express. */
+        @SuppressLint("AllUpper")
+        public static final int DTS_HDProfileLBR = 0x2;
+        /** DTS codec profile for DTS-HD Master Audio */
+        @SuppressLint("AllUpper")
+        public static final int DTS_HDProfileMA = 0x4;
+        /** DTS codec profile for DTS:X Profile 1 */
+        @SuppressLint("AllUpper")
+        public static final int DTS_UHDProfileP1 = 0x1;
+        /** DTS codec profile for DTS:X Profile 2 */
+        @SuppressLint("AllUpper")
+        public static final int DTS_UHDProfileP2 = 0x2;
+
         /**
          * The profile of the media content. Depending on the type of media this can be
          * one of the profile values defined in this class.
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index f15f443..1e270b1 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -826,6 +826,19 @@
         if(!isInternalRingtoneUri(ringtoneUri)) {
             ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId());
         }
+
+        final String mimeType = resolver.getType(ringtoneUri);
+        if (mimeType == null) {
+            Log.e(TAG, "setActualDefaultRingtoneUri for URI:" + ringtoneUri
+                    + " ignored: failure to find mimeType (no access from this context?)");
+            return;
+        }
+        if (!(mimeType.startsWith("audio/") || mimeType.equals("application/ogg"))) {
+            Log.e(TAG, "setActualDefaultRingtoneUri for URI:" + ringtoneUri
+                    + " ignored: associated mimeType:" + mimeType + " is not an audio type");
+            return;
+        }
+
         Settings.System.putStringForUser(resolver, setting,
                 ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId());
 
diff --git a/packages/CredentialManager/res/drawable/ic_passkeys_onboarding.png b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding.png
new file mode 100644
index 0000000..388d098
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding.png
Binary files differ
diff --git a/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_device.xml b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_device.xml
new file mode 100644
index 0000000..9e4f424
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_device.xml
@@ -0,0 +1,32 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="VectorPath"
+    android:name="vector"
+    android:width="21dp"
+    android:height="17dp"
+    android:viewportWidth="21"
+    android:viewportHeight="17">
+    <path
+        android:name="path"
+        android:pathData="M 4 2.941 L 20 2.941 L 20 0.941 L 4 0.941 C 2.9 0.941 2 1.841 2 2.941 L 2 13.941 L 0 13.941 L 0 16.941 L 11 16.941 L 11 13.941 L 4 13.941 L 4 2.941 Z M 20 4.941 L 14 4.941 C 13.45 4.941 13 5.391 13 5.941 L 13 15.941 C 13 16.491 13.45 16.941 14 16.941 L 20 16.941 C 20.55 16.941 21 16.491 21 15.941 L 21 5.941 C 21 5.391 20.55 4.941 20 4.941 Z M 15 13.941 L 19 13.941 L 19 6.941 L 15 6.941 L 15 13.941 Z"
+        android:fillColor="#5f6368"
+        android:strokeWidth="1"
+        android:fillType="evenOdd"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_fingerprint.xml b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_fingerprint.xml
new file mode 100644
index 0000000..b6ee4f9
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_fingerprint.xml
@@ -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.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="VectorPath"
+    android:name="vector"
+    android:width="19dp"
+    android:height="21dp"
+    android:viewportWidth="19"
+    android:viewportHeight="21">
+    <path
+        android:name="path"
+        android:pathData="M 0.25 8.591 C 0.133 8.508 0.058 8.408 0.025 8.291 C 0.008 8.158 0.05 8.025 0.15 7.891 C 1.183 6.475 2.475 5.375 4.025 4.591 C 5.592 3.808 7.258 3.416 9.025 3.416 C 10.792 3.416 12.458 3.8 14.025 4.566 C 15.592 5.316 16.9 6.408 17.95 7.841 C 18.067 7.991 18.1 8.125 18.05 8.241 C 18.017 8.358 17.95 8.458 17.85 8.541 C 17.75 8.625 17.633 8.666 17.5 8.666 C 17.367 8.65 17.25 8.575 17.15 8.441 C 16.233 7.141 15.05 6.15 13.6 5.466 C 12.167 4.766 10.642 4.416 9.025 4.416 C 7.408 4.416 5.892 4.766 4.475 5.466 C 3.058 6.15 1.883 7.141 0.95 8.441 C 0.85 8.591 0.733 8.675 0.6 8.691 C 0.467 8.708 0.35 8.675 0.25 8.591 Z M 11.85 20.916 C 10.117 20.483 8.7 19.625 7.6 18.341 C 6.5 17.041 5.95 15.458 5.95 13.591 C 5.95 12.758 6.25 12.058 6.85 11.491 C 7.45 10.925 8.175 10.641 9.025 10.641 C 9.875 10.641 10.6 10.925 11.2 11.491 C 11.8 12.058 12.1 12.758 12.1 13.591 C 12.1 14.141 12.308 14.608 12.725 14.991 C 13.142 15.358 13.633 15.541 14.2 15.541 C 14.767 15.541 15.25 15.358 15.65 14.991 C 16.05 14.608 16.25 14.141 16.25 13.591 C 16.25 11.658 15.542 10.033 14.125 8.716 C 12.708 7.4 11.017 6.741 9.05 6.741 C 7.083 6.741 5.392 7.4 3.975 8.716 C 2.558 10.033 1.85 11.65 1.85 13.566 C 1.85 13.966 1.883 14.466 1.95 15.066 C 2.033 15.666 2.217 16.366 2.5 17.166 C 2.55 17.316 2.542 17.45 2.475 17.566 C 2.425 17.683 2.333 17.766 2.2 17.816 C 2.067 17.866 1.933 17.866 1.8 17.816 C 1.683 17.75 1.6 17.65 1.55 17.516 C 1.3 16.866 1.117 16.225 1 15.591 C 0.9 14.941 0.85 14.275 0.85 13.591 C 0.85 11.375 1.65 9.516 3.25 8.016 C 4.867 6.516 6.792 5.766 9.025 5.766 C 11.275 5.766 13.208 6.516 14.825 8.016 C 16.442 9.516 17.25 11.375 17.25 13.591 C 17.25 14.425 16.95 15.125 16.35 15.691 C 15.767 16.241 15.05 16.516 14.2 16.516 C 13.35 16.516 12.617 16.241 12 15.691 C 11.4 15.125 11.1 14.425 11.1 13.591 C 11.1 13.041 10.892 12.583 10.475 12.216 C 10.075 11.833 9.592 11.641 9.025 11.641 C 8.458 11.641 7.967 11.833 7.55 12.216 C 7.15 12.583 6.95 13.041 6.95 13.591 C 6.95 15.208 7.425 16.558 8.375 17.641 C 9.342 18.725 10.583 19.483 12.1 19.916 C 12.25 19.966 12.35 20.05 12.4 20.166 C 12.45 20.283 12.458 20.408 12.425 20.541 C 12.392 20.658 12.325 20.758 12.225 20.841 C 12.125 20.925 12 20.95 11.85 20.916 Z M 3.5 3.366 C 3.367 3.45 3.233 3.475 3.1 3.441 C 2.967 3.391 2.867 3.3 2.8 3.166 C 2.733 3.033 2.717 2.916 2.75 2.816 C 2.783 2.7 2.867 2.6 3 2.516 C 3.933 2.016 4.908 1.633 5.925 1.366 C 6.942 1.1 7.975 0.966 9.025 0.966 C 10.092 0.966 11.133 1.1 12.15 1.366 C 13.167 1.616 14.15 1.983 15.1 2.466 C 15.25 2.55 15.333 2.65 15.35 2.766 C 15.383 2.883 15.375 3 15.325 3.116 C 15.275 3.233 15.192 3.325 15.075 3.391 C 14.958 3.458 14.817 3.45 14.65 3.366 C 13.767 2.916 12.85 2.575 11.9 2.341 C 10.967 2.091 10.008 1.966 9.025 1.966 C 8.058 1.966 7.108 2.083 6.175 2.316 C 5.242 2.533 4.35 2.883 3.5 3.366 Z M 6.45 20.566 C 5.467 19.533 4.708 18.483 4.175 17.416 C 3.658 16.333 3.4 15.058 3.4 13.591 C 3.4 12.075 3.95 10.8 5.05 9.766 C 6.15 8.716 7.475 8.191 9.025 8.191 C 10.575 8.191 11.908 8.716 13.025 9.766 C 14.142 10.8 14.7 12.075 14.7 13.591 C 14.7 13.741 14.65 13.866 14.55 13.966 C 14.467 14.05 14.35 14.091 14.2 14.091 C 14.067 14.091 13.95 14.05 13.85 13.966 C 13.75 13.866 13.7 13.741 13.7 13.591 C 13.7 12.341 13.233 11.3 12.3 10.466 C 11.383 9.616 10.292 9.191 9.025 9.191 C 7.758 9.191 6.667 9.616 5.75 10.466 C 4.85 11.3 4.4 12.341 4.4 13.591 C 4.4 14.941 4.633 16.091 5.1 17.041 C 5.567 17.975 6.25 18.916 7.15 19.866 C 7.25 19.966 7.3 20.083 7.3 20.216 C 7.3 20.35 7.25 20.466 7.15 20.566 C 7.05 20.666 6.933 20.716 6.8 20.716 C 6.667 20.716 6.55 20.666 6.45 20.566 Z M 14 18.866 C 12.517 18.866 11.225 18.366 10.125 17.366 C 9.042 16.366 8.5 15.108 8.5 13.591 C 8.5 13.458 8.542 13.341 8.625 13.241 C 8.725 13.141 8.85 13.091 9 13.091 C 9.15 13.091 9.267 13.141 9.35 13.241 C 9.45 13.341 9.5 13.458 9.5 13.591 C 9.5 14.841 9.95 15.866 10.85 16.666 C 11.75 17.466 12.8 17.866 14 17.866 C 14.1 17.866 14.242 17.858 14.425 17.841 C 14.608 17.825 14.8 17.8 15 17.766 C 15.15 17.733 15.275 17.758 15.375 17.841 C 15.492 17.908 15.567 18.016 15.6 18.166 C 15.633 18.3 15.608 18.416 15.525 18.516 C 15.442 18.616 15.333 18.683 15.2 18.716 C 14.9 18.8 14.633 18.85 14.4 18.866 L 14 18.866 Z"
+        android:fillColor="#5e6144"
+        android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_password.xml b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_password.xml
new file mode 100644
index 0000000..61800f1
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_passkeys_onboarding_password.xml
@@ -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.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="VectorPath"
+    android:name="vector"
+    android:width="20dp"
+    android:height="17dp"
+    android:viewportWidth="20"
+    android:viewportHeight="17">
+    <path
+        android:name="path"
+        android:pathData="M 2 16.941 C 1.45 16.941 0.975 16.75 0.575 16.366 C 0.192 15.966 0 15.491 0 14.941 L 0 2.941 C 0 2.391 0.192 1.925 0.575 1.541 C 0.975 1.141 1.45 0.941 2 0.941 L 18 0.941 C 18.55 0.941 19.017 1.141 19.4 1.541 C 19.8 1.925 20 2.391 20 2.941 L 20 14.941 C 20 15.491 19.8 15.966 19.4 16.366 C 19.017 16.75 18.55 16.941 18 16.941 L 2 16.941 Z M 4.5 11.941 L 5.65 11.941 L 5.65 5.941 L 4.75 5.941 L 3 7.191 L 3.6 8.091 L 4.5 7.441 L 4.5 11.941 Z M 7.6 11.941 L 11.5 11.941 L 11.5 10.941 L 9.15 10.941 L 9.1 10.891 C 9.45 10.558 9.733 10.275 9.95 10.041 C 10.183 9.808 10.367 9.625 10.5 9.491 C 10.8 9.191 11.025 8.891 11.175 8.591 C 11.325 8.291 11.4 7.975 11.4 7.641 C 11.4 7.158 11.217 6.758 10.85 6.441 C 10.483 6.108 10.017 5.941 9.45 5.941 C 9.017 5.941 8.625 6.066 8.275 6.316 C 7.925 6.566 7.683 6.891 7.55 7.291 L 8.55 7.691 C 8.633 7.475 8.75 7.308 8.9 7.191 C 9.067 7.058 9.25 6.991 9.45 6.991 C 9.7 6.991 9.9 7.058 10.05 7.191 C 10.217 7.325 10.3 7.491 10.3 7.691 C 10.3 7.875 10.267 8.05 10.2 8.216 C 10.133 8.366 9.983 8.558 9.75 8.791 L 8.95 9.591 C 8.6 9.941 8.15 10.391 7.6 10.941 L 7.6 11.941 Z M 15 11.941 C 15.6 11.941 16.083 11.775 16.45 11.441 C 16.817 11.108 17 10.675 17 10.141 C 17 9.841 16.917 9.575 16.75 9.341 C 16.583 9.108 16.35 8.925 16.05 8.791 L 16.05 8.741 C 16.283 8.608 16.467 8.441 16.6 8.241 C 16.733 8.025 16.8 7.775 16.8 7.491 C 16.8 7.041 16.625 6.675 16.275 6.391 C 15.925 6.091 15.483 5.941 14.95 5.941 C 14.533 5.941 14.142 6.066 13.775 6.316 C 13.425 6.55 13.2 6.841 13.1 7.191 L 14.1 7.591 C 14.167 7.391 14.275 7.233 14.425 7.116 C 14.575 7 14.75 6.941 14.95 6.941 C 15.167 6.941 15.342 7.008 15.475 7.141 C 15.625 7.258 15.7 7.408 15.7 7.591 C 15.7 7.825 15.617 8.008 15.45 8.141 C 15.283 8.275 15.067 8.341 14.8 8.341 L 14.35 8.341 L 14.35 9.341 L 14.85 9.341 C 15.183 9.341 15.442 9.408 15.625 9.541 C 15.808 9.675 15.9 9.858 15.9 10.091 C 15.9 10.308 15.808 10.5 15.625 10.666 C 15.442 10.816 15.233 10.891 15 10.891 C 14.717 10.891 14.5 10.833 14.35 10.716 C 14.2 10.583 14.067 10.358 13.95 10.041 L 12.95 10.441 C 13.067 10.925 13.3 11.3 13.65 11.566 C 14.017 11.816 14.467 11.941 15 11.941 Z M 2 14.941 L 18 14.941 L 18 2.941 L 2 2.941 L 2 14.941 Z M 2 14.941 L 2 2.941 L 2 14.941 Z"
+        android:fillColor="#5e6144"
+        android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 8c9023c..870d983 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -18,9 +18,13 @@
   <!-- This appears as a text button where users can click to save this credential to another device. [CHAR LIMIT=80] -->
   <string name="string_save_to_another_device">Save to another device</string>
   <!-- This appears as the title of the modal bottom sheet introducing what is passkey to users. [CHAR LIMIT=200] -->
-  <string name="passkey_creation_intro_title">A simple way to sign in safely</string>
-  <!-- This appears as the description body of the modal bottom sheet introducing what is passkey to users. [CHAR LIMIT=200] -->
-  <string name="passkey_creation_intro_body">Use your fingerprint, face or screen lock to sign in with a unique passkey that can’t be forgotten or stolen. Learn more</string>
+  <string name="passkey_creation_intro_title">Safer with passkeys</string>
+  <!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the passwords side. [CHAR LIMIT=200] -->
+  <string name="passkey_creation_intro_body_password">No need to create or remember complex passwords</string>
+  <!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the safety side. [CHAR LIMIT=200] -->
+  <string name="passkey_creation_intro_body_fingerprint">Use your fingerprint, face, or screen lock to create a unique passkey</string>
+  <!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the using other devices side. [CHAR LIMIT=200] -->
+  <string name="passkey_creation_intro_body_device">Passkeys are saved to a password manager, so you can sign in on other devices</string>
   <!-- This appears as the title of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] -->
   <string name="choose_provider_title">Choose where to <xliff:g id="createTypes" example="create your passkeys">%1$s</xliff:g></string>
   <!-- Create types which are inserted as a placeholder for string choose_provider_title. [CHAR LIMIT=200] -->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 1db1d1c..2780c3c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -40,12 +40,9 @@
 import android.os.Bundle
 import android.os.ResultReceiver
 import android.service.credentials.CredentialProviderService
-import com.android.credentialmanager.createflow.ActiveEntry
 import com.android.credentialmanager.createflow.CreateCredentialUiState
-import com.android.credentialmanager.createflow.CreateScreenState
 import com.android.credentialmanager.createflow.EnabledProviderInfo
 import com.android.credentialmanager.createflow.RemoteInfo
-import com.android.credentialmanager.createflow.RequestDisplayInfo
 import com.android.credentialmanager.getflow.GetCredentialUiState
 import com.android.credentialmanager.getflow.GetScreenState
 import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle
@@ -146,20 +143,30 @@
       providerDisabledList, context)
     var defaultProvider: EnabledProviderInfo? = null
     var remoteEntry: RemoteInfo? = null
+    var createOptionSize = 0
+    var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
     providerEnabledList.forEach{providerInfo -> providerInfo.createOptions =
       providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed()
       if (providerInfo.isDefault) {defaultProvider = providerInfo}
       if (providerInfo.remoteEntry != null) {
         remoteEntry = providerInfo.remoteEntry!!
       }
+      if (providerInfo.createOptions.isNotEmpty()) {
+        createOptionSize += providerInfo.createOptions.size
+        lastSeenProviderWithNonEmptyCreateOptions = providerInfo
+      }
     }
     return CreateCredentialUiState(
       enabledProviders = providerEnabledList,
       disabledProviders = providerDisabledList,
-      toCreateScreenState(requestDisplayInfo, defaultProvider, remoteEntry),
+      CreateFlowUtils.toCreateScreenState(
+        createOptionSize, false,
+        requestDisplayInfo, defaultProvider, remoteEntry),
       requestDisplayInfo,
       false,
-      toActiveEntry(defaultProvider, remoteEntry),
+      CreateFlowUtils.toActiveEntry(
+        /*defaultProvider=*/defaultProvider, createOptionSize,
+        lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
     )
   }
 
@@ -194,6 +201,7 @@
         .setRemoteEntry(
           newRemoteEntry("key2", "subkey-1")
         )
+        .setIsDefaultProvider(true)
         .build(),
       CreateCredentialProviderData
         .Builder("com.dashlane")
@@ -517,38 +525,4 @@
       "tribank.us"
     )
   }
-
-  private fun toCreateScreenState(
-    requestDisplayInfo: RequestDisplayInfo,
-    defaultProvider: EnabledProviderInfo?,
-    remoteEntry: RemoteInfo?,
-  ): CreateScreenState {
-    return if (
-      defaultProvider != null && defaultProvider.createOptions.isEmpty() && remoteEntry != null
-    ){
-      CreateScreenState.EXTERNAL_ONLY_SELECTION
-    } else if (defaultProvider == null || defaultProvider.createOptions.isEmpty()) {
-      if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL) {
-        CreateScreenState.PASSKEY_INTRO
-      } else {
-        CreateScreenState.PROVIDER_SELECTION
-      }
-    } else {
-      CreateScreenState.CREATION_OPTION_SELECTION
-    }
-  }
-
-  private fun toActiveEntry(
-    defaultProvider: EnabledProviderInfo?,
-    remoteEntry: RemoteInfo?,
-  ): ActiveEntry? {
-    return if (
-      defaultProvider != null && defaultProvider.createOptions.isNotEmpty()
-    ) {
-      ActiveEntry(defaultProvider, defaultProvider.createOptions.first())
-    } else if (
-      defaultProvider != null && defaultProvider.createOptions.isEmpty() && remoteEntry != null) {
-      ActiveEntry(defaultProvider, remoteEntry)
-    } else null
-  }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index d324f87..6a4c599 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -69,6 +69,7 @@
         )
         providerActivityResult.value?.let {
           viewModel.onProviderActivityResult(it)
+          providerActivityResult.value = null
         }
         CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
       }
@@ -80,6 +81,7 @@
         )
         providerActivityResult.value?.let {
           viewModel.onProviderActivityResult(it)
+          providerActivityResult.value = null
         }
         GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
       }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 357c55d..0d7e819 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -28,6 +28,9 @@
 import com.android.credentialmanager.createflow.CreateOptionInfo
 import com.android.credentialmanager.createflow.RemoteInfo
 import com.android.credentialmanager.createflow.RequestDisplayInfo
+import com.android.credentialmanager.createflow.EnabledProviderInfo
+import com.android.credentialmanager.createflow.CreateScreenState
+import com.android.credentialmanager.createflow.ActiveEntry
 import com.android.credentialmanager.getflow.ActionEntryInfo
 import com.android.credentialmanager.getflow.AuthenticationEntryInfo
 import com.android.credentialmanager.getflow.CredentialEntryInfo
@@ -36,6 +39,7 @@
 import com.android.credentialmanager.jetpack.developer.CreateCredentialRequest
 import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest
 import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
 import com.android.credentialmanager.jetpack.provider.ActionUi
 import com.android.credentialmanager.jetpack.provider.CredentialEntryUi
 import com.android.credentialmanager.jetpack.provider.SaveEntryUi
@@ -243,7 +247,8 @@
             createCredentialRequestJetpack.password,
             createCredentialRequestJetpack.type,
             requestInfo.appPackageName,
-            context.getDrawable(R.drawable.ic_password)!!
+            context.getDrawable(R.drawable.ic_password)!!,
+            requestInfo.isFirstUsage
           )
         }
         is CreatePublicKeyCredentialRequest -> {
@@ -261,7 +266,8 @@
             displayName,
             createCredentialRequestJetpack.type,
             requestInfo.appPackageName,
-            context.getDrawable(R.drawable.ic_passkey)!!)
+            context.getDrawable(R.drawable.ic_passkey)!!,
+            requestInfo.isFirstUsage)
         }
         // TODO: correctly parsing for other sign-ins
         else -> {
@@ -270,11 +276,58 @@
             "Elisa Beckett",
             "other-sign-ins",
             requestInfo.appPackageName,
-            context.getDrawable(R.drawable.ic_other_sign_in)!!)
+            context.getDrawable(R.drawable.ic_other_sign_in)!!,
+            requestInfo.isFirstUsage)
         }
       }
     }
 
+    fun toCreateScreenState(
+      createOptionSize: Int,
+      isOnPasskeyIntroStateAlready: Boolean,
+      requestDisplayInfo: RequestDisplayInfo,
+      defaultProvider: EnabledProviderInfo?,
+      remoteEntry: RemoteInfo?,
+    ): CreateScreenState {
+      return if (requestDisplayInfo.isFirstUsage && requestDisplayInfo
+          .type == TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) {
+        CreateScreenState.PASSKEY_INTRO
+      } else if (
+        (defaultProvider == null || defaultProvider.createOptions.isEmpty()
+                ) && createOptionSize > 1) {
+        CreateScreenState.PROVIDER_SELECTION
+      } else if (
+        ((defaultProvider == null || defaultProvider.createOptions.isEmpty()
+                ) && createOptionSize == 1) || (
+                defaultProvider != null && defaultProvider.createOptions.isNotEmpty())) {
+        CreateScreenState.CREATION_OPTION_SELECTION
+      } else if (createOptionSize == 0 && remoteEntry != null) {
+        CreateScreenState.EXTERNAL_ONLY_SELECTION
+      } else {
+          // TODO: properly handle error and gracefully finish itself
+          throw java.lang.IllegalStateException("Empty provider list.")
+      }
+    }
+
+   fun toActiveEntry(
+      defaultProvider: EnabledProviderInfo?,
+      createOptionSize: Int,
+      lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo?,
+      remoteEntry: RemoteInfo?,
+    ): ActiveEntry? {
+      return if (
+        defaultProvider != null && defaultProvider.createOptions.isEmpty() && remoteEntry != null) {
+        ActiveEntry(defaultProvider, remoteEntry)
+      } else if (
+        defaultProvider != null && defaultProvider.createOptions.isNotEmpty()
+      ) {
+        ActiveEntry(defaultProvider, defaultProvider.createOptions.first())
+      } else if (createOptionSize == 1) {
+        ActiveEntry(lastSeenProviderWithNonEmptyCreateOptions!!,
+          lastSeenProviderWithNonEmptyCreateOptions.createOptions.first())
+      } else null
+    }
+
     private fun toCreationOptionInfoList(
       providerId: String,
       creationEntries: List<Entry>,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index d8dd1a7..b417b38 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -60,12 +60,6 @@
     viewModel: CreateCredentialViewModel,
     providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
 ) {
-    val selectEntryCallback: (EntryInfo) -> Unit = {
-        viewModel.onEntrySelected(it, providerActivityLauncher)
-    }
-    val confirmEntryCallback: () -> Unit = {
-        viewModel.onConfirmEntrySelected(providerActivityLauncher)
-    }
     val state = rememberModalBottomSheetState(
         initialValue = ModalBottomSheetValue.Expanded,
         skipHalfExpanded = true
@@ -75,53 +69,57 @@
         sheetState = state,
         sheetContent = {
             val uiState = viewModel.uiState
-            when (uiState.currentScreenState) {
-                CreateScreenState.PASSKEY_INTRO -> ConfirmationCard(
-                    onConfirm = viewModel::onConfirmIntro,
-                    onCancel = viewModel::onCancel,
-                )
-                CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard(
-                    requestDisplayInfo = uiState.requestDisplayInfo,
-                    enabledProviderList = uiState.enabledProviders,
-                    disabledProviderList = uiState.disabledProviders,
-                    onCancel = viewModel::onCancel,
-                    onOptionSelected = viewModel::onEntrySelectedFromFirstUseScreen,
-                    onDisabledPasswordManagerSelected =
+            if (!uiState.hidden) {
+                when (uiState.currentScreenState) {
+                    CreateScreenState.PASSKEY_INTRO -> ConfirmationCard(
+                        onConfirm = viewModel::onConfirmIntro,
+                        onCancel = viewModel::onCancel,
+                    )
+                    CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard(
+                        requestDisplayInfo = uiState.requestDisplayInfo,
+                        enabledProviderList = uiState.enabledProviders,
+                        disabledProviderList = uiState.disabledProviders,
+                        onCancel = viewModel::onCancel,
+                        onOptionSelected = viewModel::onEntrySelectedFromFirstUseScreen,
+                        onDisabledPasswordManagerSelected =
                         viewModel::onDisabledPasswordManagerSelected,
-                    onRemoteEntrySelected = selectEntryCallback,
-                )
-                CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
-                    requestDisplayInfo = uiState.requestDisplayInfo,
-                    enabledProviderList = uiState.enabledProviders,
-                    providerInfo = uiState.activeEntry?.activeProvider!!,
-                    createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo,
-                    showActiveEntryOnly = uiState.showActiveEntryOnly,
-                    onOptionSelected = selectEntryCallback,
-                    onConfirm = confirmEntryCallback,
-                    onCancel = viewModel::onCancel,
-                    onMoreOptionsSelected = viewModel::onMoreOptionsSelected,
-                )
-                CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
-                    requestDisplayInfo = uiState.requestDisplayInfo,
-                    enabledProviderList = uiState.enabledProviders,
-                    disabledProviderList = uiState.disabledProviders,
-                    onBackButtonSelected = viewModel::onBackButtonSelected,
-                    onOptionSelected = viewModel::onEntrySelectedFromMoreOptionScreen,
-                    onDisabledPasswordManagerSelected =
+                        onRemoteEntrySelected = viewModel::onEntrySelected,
+                    )
+                    CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
+                        requestDisplayInfo = uiState.requestDisplayInfo,
+                        enabledProviderList = uiState.enabledProviders,
+                        providerInfo = uiState.activeEntry?.activeProvider!!,
+                        createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo,
+                        showActiveEntryOnly = uiState.showActiveEntryOnly,
+                        onOptionSelected = viewModel::onEntrySelected,
+                        onConfirm = viewModel::onConfirmEntrySelected,
+                        onCancel = viewModel::onCancel,
+                        onMoreOptionsSelected = viewModel::onMoreOptionsSelected,
+                    )
+                    CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
+                        requestDisplayInfo = uiState.requestDisplayInfo,
+                        enabledProviderList = uiState.enabledProviders,
+                        disabledProviderList = uiState.disabledProviders,
+                        onBackButtonSelected = viewModel::onBackButtonSelected,
+                        onOptionSelected = viewModel::onEntrySelectedFromMoreOptionScreen,
+                        onDisabledPasswordManagerSelected =
                         viewModel::onDisabledPasswordManagerSelected,
-                    onRemoteEntrySelected = selectEntryCallback,
-                )
-                CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
-                    providerInfo = uiState.activeEntry?.activeProvider!!,
-                    onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected
-                )
-                CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard(
-                    requestDisplayInfo = uiState.requestDisplayInfo,
-                    activeRemoteEntry = uiState.activeEntry?.activeEntryInfo!!,
-                    onOptionSelected = selectEntryCallback,
-                    onConfirm = confirmEntryCallback,
-                    onCancel = viewModel::onCancel,
-                )
+                        onRemoteEntrySelected = viewModel::onEntrySelected,
+                    )
+                    CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
+                        providerInfo = uiState.activeEntry?.activeProvider!!,
+                        onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected
+                    )
+                    CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard(
+                        requestDisplayInfo = uiState.requestDisplayInfo,
+                        activeRemoteEntry = uiState.activeEntry?.activeEntryInfo!!,
+                        onOptionSelected = viewModel::onEntrySelected,
+                        onConfirm = viewModel::onConfirmEntrySelected,
+                        onCancel = viewModel::onCancel,
+                    )
+                }
+            } else if (uiState.selectedEntry != null && !uiState.providerActivityPending) {
+                viewModel.launchProviderUi(providerActivityLauncher)
             }
         },
         scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.8f),
@@ -142,12 +140,11 @@
 ) {
     ContainerCard() {
         Column() {
-            Icon(
-                painter = painterResource(R.drawable.ic_passkey),
+            Image(
+                painter = painterResource(R.drawable.ic_passkeys_onboarding),
                 contentDescription = null,
-                tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
                 modifier = Modifier.align(alignment = Alignment.CenterHorizontally)
-                    .padding(top = 24.dp, bottom = 12.dp)
+                    .padding(top = 24.dp, bottom = 12.dp).size(316.dp, 168.dp)
             )
             TextOnSurface(
                 text = stringResource(R.string.passkey_creation_intro_title),
@@ -161,11 +158,62 @@
                 thickness = 16.dp,
                 color = Color.Transparent
             )
-            TextSecondary(
-                text = stringResource(R.string.passkey_creation_intro_body),
-                style = MaterialTheme.typography.bodyLarge,
-                modifier = Modifier.padding(horizontal = 28.dp),
+            Row(
+                horizontalArrangement = Arrangement.SpaceBetween,
+                verticalAlignment = Alignment.CenterVertically,
+                modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+            ) {
+                Image(
+                    modifier = Modifier.size(24.dp),
+                    painter = painterResource(R.drawable.ic_passkeys_onboarding_password),
+                    contentDescription = null
+                )
+                TextSecondary(
+                    text = stringResource(R.string.passkey_creation_intro_body_password),
+                    style = MaterialTheme.typography.bodyMedium,
+                    modifier = Modifier.padding(start = 16.dp),
+                )
+            }
+            Divider(
+                thickness = 16.dp,
+                color = Color.Transparent
             )
+            Row(
+                horizontalArrangement = Arrangement.SpaceBetween,
+                verticalAlignment = Alignment.CenterVertically,
+                modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+            ) {
+                Image(
+                    modifier = Modifier.size(24.dp),
+                    painter = painterResource(R.drawable.ic_passkeys_onboarding_fingerprint),
+                    contentDescription = null
+                )
+                TextSecondary(
+                    text = stringResource(R.string.passkey_creation_intro_body_fingerprint),
+                    style = MaterialTheme.typography.bodyMedium,
+                    modifier = Modifier.padding(start = 16.dp),
+                )
+            }
+            Divider(
+                thickness = 16.dp,
+                color = Color.Transparent
+            )
+            Row(
+                horizontalArrangement = Arrangement.SpaceBetween,
+                verticalAlignment = Alignment.CenterVertically,
+                modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+            ) {
+                Image(
+                    modifier = Modifier.size(24.dp),
+                    painter = painterResource(R.drawable.ic_passkeys_onboarding_device),
+                    contentDescription = null
+                )
+                TextSecondary(
+                    text = stringResource(R.string.passkey_creation_intro_body_device),
+                    style = MaterialTheme.typography.bodyMedium,
+                    modifier = Modifier.padding(start = 16.dp),
+                )
+            }
             Divider(
                 thickness = 32.dp,
                 color = Color.Transparent
@@ -270,7 +318,7 @@
                             MoreOptionsDisabledProvidersRow(
                                 disabledProviders = disabledProviderList,
                                 onDisabledPasswordManagerSelected =
-                                    onDisabledPasswordManagerSelected,
+                                onDisabledPasswordManagerSelected,
                             )
                         }
                     }
@@ -700,7 +748,7 @@
                 },
                 contentDescription = null,
                 tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
-                modifier = Modifier.padding(start = 18.dp).size(32.dp)
+                modifier = Modifier.padding(horizontal = 18.dp).size(32.dp)
             )
         },
         label = {
@@ -762,7 +810,7 @@
         onClick = onOptionSelected,
         icon = {
             Image(
-                modifier = Modifier.size(32.dp).padding(start = 16.dp),
+                modifier = Modifier.padding(horizontal = 16.dp).size(32.dp),
                 bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
                 contentDescription = null
             )
@@ -772,17 +820,17 @@
                 TextOnSurfaceVariant(
                     text = providerInfo.displayName,
                     style = MaterialTheme.typography.titleLarge,
-                    modifier = Modifier.padding(top = 16.dp, start = 16.dp),
+                    modifier = Modifier.padding(top = 16.dp),
                 )
                 if (createOptionInfo.userProviderDisplayName != null) {
                     TextSecondary(
                         text = createOptionInfo.userProviderDisplayName,
                         style = MaterialTheme.typography.bodyMedium,
-                        modifier = Modifier.padding(start = 16.dp),
                     )
                 }
                 if (createOptionInfo.passwordCount != null &&
-                    createOptionInfo.passkeyCount != null) {
+                    createOptionInfo.passkeyCount != null
+                ) {
                     TextSecondary(
                         text =
                         stringResource(
@@ -791,7 +839,7 @@
                             createOptionInfo.passkeyCount
                         ),
                         style = MaterialTheme.typography.bodyMedium,
-                        modifier = Modifier.padding(bottom = 16.dp, start = 16.dp),
+                        modifier = Modifier.padding(bottom = 16.dp),
                     )
                 } else if (createOptionInfo.passwordCount != null) {
                     TextSecondary(
@@ -801,7 +849,7 @@
                             createOptionInfo.passwordCount
                         ),
                         style = MaterialTheme.typography.bodyMedium,
-                        modifier = Modifier.padding(bottom = 16.dp, start = 16.dp),
+                        modifier = Modifier.padding(bottom = 16.dp),
                     )
                 } else if (createOptionInfo.passkeyCount != null) {
                     TextSecondary(
@@ -811,7 +859,7 @@
                             createOptionInfo.passkeyCount
                         ),
                         style = MaterialTheme.typography.bodyMedium,
-                        modifier = Modifier.padding(bottom = 16.dp, start = 16.dp),
+                        modifier = Modifier.padding(bottom = 16.dp),
                     )
                 } else if (createOptionInfo.totalCredentialCount != null) {
                     // TODO: Handle the case when there is total count
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
index 393cf7d..518aaee 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.credentialmanager.createflow
 
+import android.app.Activity
 import android.util.Log
 import androidx.activity.compose.ManagedActivityResultLauncher
 import androidx.activity.result.ActivityResult
@@ -26,6 +27,7 @@
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.CreateFlowUtils
 import com.android.credentialmanager.CredentialManagerRepo
 import com.android.credentialmanager.common.DialogResult
 import com.android.credentialmanager.common.ProviderActivityResult
@@ -39,6 +41,8 @@
   val showActiveEntryOnly: Boolean,
   val activeEntry: ActiveEntry? = null,
   val selectedEntry: EntryInfo? = null,
+  val hidden: Boolean = false,
+  val providerActivityPending: Boolean = false,
 )
 
 class CreateCredentialViewModel(
@@ -58,24 +62,26 @@
 
   fun onConfirmIntro() {
     var createOptionSize = 0
+    var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
+    var remoteEntry: RemoteInfo? = null
     uiState.enabledProviders.forEach {
-      enabledProvider -> createOptionSize += enabledProvider.createOptions.size}
-    uiState = if (createOptionSize > 1) {
-      uiState.copy(
-        currentScreenState = CreateScreenState.PROVIDER_SELECTION,
-        showActiveEntryOnly = true
-      )
-    } else if (createOptionSize == 1){
-      uiState.copy(
-        currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-        showActiveEntryOnly = false,
-        activeEntry = ActiveEntry(uiState.enabledProviders.first(),
-          uiState.enabledProviders.first().createOptions.first()
-        )
-      )
-    } else {
-      throw java.lang.IllegalStateException("Empty provider list.")
+      enabledProvider ->
+      if (enabledProvider.createOptions.isNotEmpty()) {
+        createOptionSize += enabledProvider.createOptions.size
+        lastSeenProviderWithNonEmptyCreateOptions = enabledProvider
+      }
+      if (enabledProvider.remoteEntry != null) {
+        remoteEntry = enabledProvider.remoteEntry!!
+      }
     }
+    uiState = uiState.copy(
+      currentScreenState = CreateFlowUtils.toCreateScreenState(
+        createOptionSize, true,
+        uiState.requestDisplayInfo, null, remoteEntry),
+      showActiveEntryOnly = createOptionSize > 1,
+      activeEntry = CreateFlowUtils.toActiveEntry(
+        null, createOptionSize, lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
+    )
   }
 
   fun getProviderInfoByName(providerName: String): EnabledProviderInfo {
@@ -128,10 +134,7 @@
     // TODO: implement the if choose as default or not logic later
   }
 
-  fun onEntrySelected(
-    selectedEntry: EntryInfo,
-    launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
-  ) {
+  fun onEntrySelected(selectedEntry: EntryInfo) {
     val providerId = selectedEntry.providerId
     val entryKey = selectedEntry.entryKey
     val entrySubkey = selectedEntry.entrySubkey
@@ -139,10 +142,10 @@
       "Account Selector", "Option selected for entry: " +
               " {provider=$providerId, key=$entryKey, subkey=$entrySubkey")
     if (selectedEntry.pendingIntent != null) {
-      uiState = uiState.copy(selectedEntry = selectedEntry)
-      val intentSenderRequest = IntentSenderRequest.Builder(selectedEntry.pendingIntent)
-        .setFillInIntent(selectedEntry.fillInIntent).build()
-      launcher.launch(intentSenderRequest)
+      uiState = uiState.copy(
+        selectedEntry = selectedEntry,
+        hidden = true,
+      )
     } else {
       CredentialManagerRepo.getInstance().onOptionSelected(
         providerId,
@@ -155,12 +158,26 @@
     }
   }
 
-  fun onConfirmEntrySelected(
+  fun launchProviderUi(
     launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
   ) {
+    val entry = uiState.selectedEntry
+    if (entry != null && entry.pendingIntent != null) {
+      uiState = uiState.copy(
+        providerActivityPending = true,
+      )
+      val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
+        .setFillInIntent(entry.fillInIntent).build()
+      launcher.launch(intentSenderRequest)
+    } else {
+      Log.w("Account Selector", "No provider UI to launch")
+    }
+  }
+
+  fun onConfirmEntrySelected() {
     val selectedEntry = uiState.activeEntry?.activeEntryInfo
     if (selectedEntry != null) {
-      onEntrySelected(selectedEntry, launcher)
+      onEntrySelected(selectedEntry)
     } else {
       Log.w("Account Selector",
         "Illegal state: confirm is pressed but activeEntry isn't set.")
@@ -174,21 +191,30 @@
     val entry = uiState.selectedEntry
     val resultCode = providerActivityResult.resultCode
     val resultData = providerActivityResult.data
-    if (entry != null) {
-      val providerId = entry.providerId
-      Log.d("Account Selector", "Got provider activity result: {provider=" +
-              "$providerId, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
-              "resultCode=$resultCode, resultData=$resultData}"
-      )
-      CredentialManagerRepo.getInstance().onOptionSelected(
-        providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData,
+    if (resultCode == Activity.RESULT_CANCELED) {
+      // Re-display the CredMan UI if the user canceled from the provider UI.
+      uiState = uiState.copy(
+        selectedEntry = null,
+        hidden = false,
+        providerActivityPending = false,
       )
     } else {
-      Log.w("Account Selector",
-        "Illegal state: received a provider result but found no matching entry.")
+      if (entry != null) {
+        val providerId = entry.providerId
+        Log.d("Account Selector", "Got provider activity result: {provider=" +
+                "$providerId, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
+                "resultCode=$resultCode, resultData=$resultData}"
+        )
+        CredentialManagerRepo.getInstance().onOptionSelected(
+          providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData,
+        )
+      } else {
+        Log.w("Account Selector",
+          "Illegal state: received a provider result but found no matching entry.")
+      }
+      dialogResult.value = DialogResult(
+        ResultState.COMPLETE,
+      )
     }
-    dialogResult.value = DialogResult(
-      ResultState.COMPLETE,
-    )
   }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 9ac524a..21abe08 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -77,6 +77,7 @@
   val type: String,
   val appDomainName: String,
   val typeIcon: Drawable,
+  val isFirstUsage: Boolean,
 )
 
 /**
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 31dc069..d6d7122 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -71,9 +71,6 @@
     viewModel: GetCredentialViewModel,
     providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
 ) {
-    val entrySelectionCallback: (EntryInfo) -> Unit = {
-        viewModel.onEntrySelected(it, providerActivityLauncher)
-    }
     val state = rememberModalBottomSheetState(
         initialValue = ModalBottomSheetValue.Expanded,
         skipHalfExpanded = true
@@ -84,20 +81,24 @@
         sheetState = state,
         sheetContent = {
             val uiState = viewModel.uiState
-            when (uiState.currentScreenState) {
-                GetScreenState.PRIMARY_SELECTION -> PrimarySelectionCard(
-                    requestDisplayInfo = uiState.requestDisplayInfo,
-                    providerDisplayInfo = uiState.providerDisplayInfo,
-                    onEntrySelected = entrySelectionCallback,
-                    onCancel = viewModel::onCancel,
-                    onMoreOptionSelected = viewModel::onMoreOptionSelected,
-                )
-                GetScreenState.ALL_SIGN_IN_OPTIONS -> AllSignInOptionCard(
-                    providerInfoList = uiState.providerInfoList,
-                    providerDisplayInfo = uiState.providerDisplayInfo,
-                    onEntrySelected = entrySelectionCallback,
-                    onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
-                )
+            if (!uiState.hidden) {
+                when (uiState.currentScreenState) {
+                    GetScreenState.PRIMARY_SELECTION -> PrimarySelectionCard(
+                        requestDisplayInfo = uiState.requestDisplayInfo,
+                        providerDisplayInfo = uiState.providerDisplayInfo,
+                        onEntrySelected = viewModel::onEntrySelected,
+                        onCancel = viewModel::onCancel,
+                        onMoreOptionSelected = viewModel::onMoreOptionSelected,
+                    )
+                    GetScreenState.ALL_SIGN_IN_OPTIONS -> AllSignInOptionCard(
+                        providerInfoList = uiState.providerInfoList,
+                        providerDisplayInfo = uiState.providerDisplayInfo,
+                        onEntrySelected = viewModel::onEntrySelected,
+                        onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
+                    )
+                }
+            } else if (uiState.selectedEntry != null && !uiState.providerActivityPending) {
+                viewModel.launchProviderUi(providerActivityLauncher)
             }
         },
         scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.8f),
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index 6dea9c2..7b80124 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.credentialmanager.getflow
 
+import android.app.Activity
 import android.util.Log
 import androidx.activity.compose.ManagedActivityResultLauncher
 import androidx.activity.result.ActivityResult
@@ -39,6 +40,8 @@
   val requestDisplayInfo: RequestDisplayInfo,
   val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
   val selectedEntry: EntryInfo? = null,
+  val hidden: Boolean = false,
+  val providerActivityPending: Boolean = false,
 )
 
 class GetCredentialViewModel(
@@ -56,17 +59,14 @@
     return dialogResult
   }
 
-  fun onEntrySelected(
-    entry: EntryInfo,
-    launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
-  ) {
+  fun onEntrySelected(entry: EntryInfo) {
     Log.d("Account Selector", "credential selected:" +
             " {provider=${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
     if (entry.pendingIntent != null) {
-      uiState = uiState.copy(selectedEntry = entry)
-      val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
-        .setFillInIntent(entry.fillInIntent).build()
-      launcher.launch(intentSenderRequest)
+      uiState = uiState.copy(
+        selectedEntry = entry,
+        hidden = true,
+      )
     } else {
       CredentialManagerRepo.getInstance().onOptionSelected(
         entry.providerId, entry.entryKey, entry.entrySubkey,
@@ -75,24 +75,49 @@
     }
   }
 
+  fun launchProviderUi(
+    launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
+  ) {
+    val entry = uiState.selectedEntry
+    if (entry != null && entry.pendingIntent != null) {
+      uiState = uiState.copy(
+        providerActivityPending = true,
+      )
+      val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
+        .setFillInIntent(entry.fillInIntent).build()
+      launcher.launch(intentSenderRequest)
+    } else {
+      Log.w("Account Selector", "No provider UI to launch")
+    }
+  }
+
   fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
     val entry = uiState.selectedEntry
     val resultCode = providerActivityResult.resultCode
     val resultData = providerActivityResult.data
-    if (entry != null) {
-      Log.d("Account Selector", "Got provider activity result: {provider=" +
-              "${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
-                "resultCode=$resultCode, resultData=$resultData}"
-      )
-      CredentialManagerRepo.getInstance().onOptionSelected(
-        entry.providerId, entry.entryKey, entry.entrySubkey,
-        resultCode, resultData,
+    if (resultCode == Activity.RESULT_CANCELED) {
+      // Re-display the CredMan UI if the user canceled from the provider UI.
+      uiState = uiState.copy(
+        selectedEntry = null,
+        hidden = false,
+        providerActivityPending = false,
       )
     } else {
-      Log.w("Account Selector",
-        "Illegal state: received a provider result but found no matching entry.")
+      if (entry != null) {
+        Log.d("Account Selector", "Got provider activity result: {provider=" +
+                "${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
+                "resultCode=$resultCode, resultData=$resultData}"
+        )
+        CredentialManagerRepo.getInstance().onOptionSelected(
+          entry.providerId, entry.entryKey, entry.entrySubkey,
+          resultCode, resultData,
+        )
+      } else {
+        Log.w("Account Selector",
+          "Illegal state: received a provider result but found no matching entry.")
+      }
+      dialogResult.value = DialogResult(ResultState.COMPLETE)
     }
-    dialogResult.value = DialogResult(ResultState.COMPLETE)
   }
 
   fun onMoreOptionSelected() {
diff --git a/packages/SettingsLib/MainSwitchPreference/Android.bp b/packages/SettingsLib/MainSwitchPreference/Android.bp
index dd29827..372a276 100644
--- a/packages/SettingsLib/MainSwitchPreference/Android.bp
+++ b/packages/SettingsLib/MainSwitchPreference/Android.bp
@@ -25,5 +25,6 @@
         "//apex_available:platform",
         "com.android.adservices",
         "com.android.cellbroadcast",
+        "com.android.healthconnect",
     ],
 }
diff --git a/packages/SettingsLib/Spa/settings.gradle b/packages/SettingsLib/Spa/settings.gradle
index b627a70..1c5a1ce 100644
--- a/packages/SettingsLib/Spa/settings.gradle
+++ b/packages/SettingsLib/Spa/settings.gradle
@@ -33,4 +33,3 @@
 include ':spa'
 include ':gallery'
 include ':testutils'
-include ':tests'
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index b1d8d0d..19963fb 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -27,6 +27,8 @@
     defaultConfig {
         minSdk MIN_SDK
         targetSdk TARGET_SDK
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
 
     sourceSets {
@@ -37,6 +39,13 @@
             res.srcDirs = ["res"]
             manifest.srcFile "AndroidManifest.xml"
         }
+        androidTest {
+            kotlin {
+                srcDir "../tests/src"
+            }
+            res.srcDirs = ["../tests/res"]
+            manifest.srcFile "../tests/AndroidManifest.xml"
+        }
     }
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
@@ -52,6 +61,11 @@
     composeOptions {
         kotlinCompilerExtensionVersion jetpack_compose_compiler_version
     }
+    buildTypes {
+        debug {
+            testCoverageEnabled = true
+        }
+    }
 }
 
 dependencies {
@@ -72,4 +86,29 @@
     api "com.google.android.material:material:1.7.0-alpha03"
     debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version"
     implementation "com.airbnb.android:lottie-compose:5.2.0"
+
+    androidTestImplementation project(":testutils")
+    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.28.1"
+}
+
+task coverageReport(type: JacocoReport, dependsOn: "connectedDebugAndroidTest") {
+    group = "Reporting"
+    description = "Generate Jacoco coverage reports after running tests."
+
+    sourceDirectories.from = files("src")
+    classDirectories.from = fileTree(
+            dir: "$buildDir/tmp/kotlin-classes/debug",
+            excludes: [
+                    "com/android/settingslib/spa/debug/**",
+
+                    // Excludes files forked from AndroidX.
+                    "com/android/settingslib/spa/widget/scaffold/CustomizedAppBar*",
+                    "com/android/settingslib/spa/widget/scaffold/TopAppBarColors*",
+
+                    // Excludes files forked from Accompanist.
+                    "com/android/settingslib/spa/framework/compose/DrawablePainter*",
+                    "com/android/settingslib/spa/framework/compose/Pager*",
+            ],
+    )
+    executionData.from = fileTree(dir: "$buildDir/outputs/code_coverage/debugAndroidTest/connected")
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/MessageFormats.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/MessageFormats.kt
new file mode 100644
index 0000000..2adfcca
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/MessageFormats.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.util
+
+import android.content.Context
+import android.content.res.Resources
+import android.icu.text.MessageFormat
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.StringRes
+import java.util.Locale
+
+@RequiresApi(Build.VERSION_CODES.N)
+fun Context.formatString(@StringRes resId: Int, vararg arguments: Pair<String, Any>): String =
+    resources.formatString(resId, *arguments)
+
+@RequiresApi(Build.VERSION_CODES.N)
+fun Resources.formatString(@StringRes resId: Int, vararg arguments: Pair<String, Any>): String =
+    MessageFormat(getString(resId), Locale.getDefault(Locale.Category.FORMAT))
+        .format(mapOf(*arguments))
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt
new file mode 100644
index 0000000..2301f04
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.spa.search
+
+/**
+ * Intent action used to identify SpaSearchProvider instances. This is used in the {@code
+ * <intent-filter>} of a {@code <provider>}.
+ */
+const val PROVIDER_INTERFACE = "android.content.action.SPA_SEARCH_PROVIDER"
+
+/** ContentProvider path for search static data */
+const val SEARCH_STATIC_DATA = "search_static_data"
+
+/** ContentProvider path for search dynamic data */
+const val SEARCH_DYNAMIC_DATA = "search_dynamic_data"
+
+/** ContentProvider path for search immutable status */
+const val SEARCH_IMMUTABLE_STATUS = "search_immutable_status"
+
+/** ContentProvider path for search mutable status */
+const val SEARCH_MUTABLE_STATUS = "search_mutable_status"
+
+/** Enum to define all column names in provider. */
+enum class ColumnEnum(val id: String) {
+    ENTRY_ID("entryId"),
+    SEARCH_TITLE("searchTitle"),
+    SEARCH_KEYWORD("searchKw"),
+    SEARCH_PATH("searchPath"),
+    INTENT_TARGET_PACKAGE("intentTargetPackage"),
+    INTENT_TARGET_CLASS("intentTargetClass"),
+    INTENT_EXTRAS("intentExtras"),
+    SLICE_URI("sliceUri"),
+    LEGACY_KEY("legacyKey"),
+    ENTRY_DISABLED("entryDisabled"),
+}
+
+/** Enum to define all queries supported in the provider. */
+@SuppressWarnings("Immutable")
+enum class QueryEnum(
+    val queryPath: String,
+    val columnNames: List<ColumnEnum>
+) {
+    SEARCH_STATIC_DATA_QUERY(
+        SEARCH_STATIC_DATA,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.SEARCH_TITLE,
+            ColumnEnum.SEARCH_KEYWORD,
+            ColumnEnum.SEARCH_PATH,
+            ColumnEnum.INTENT_TARGET_PACKAGE,
+            ColumnEnum.INTENT_TARGET_CLASS,
+            ColumnEnum.INTENT_EXTRAS,
+            ColumnEnum.SLICE_URI,
+            ColumnEnum.LEGACY_KEY
+        )
+    ),
+    SEARCH_DYNAMIC_DATA_QUERY(
+        SEARCH_DYNAMIC_DATA,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.SEARCH_TITLE,
+            ColumnEnum.SEARCH_KEYWORD,
+            ColumnEnum.SEARCH_PATH,
+            ColumnEnum.INTENT_TARGET_PACKAGE,
+            ColumnEnum.INTENT_TARGET_CLASS,
+            ColumnEnum.SLICE_URI,
+            ColumnEnum.LEGACY_KEY
+        )
+    ),
+    SEARCH_IMMUTABLE_STATUS_DATA_QUERY(
+        SEARCH_IMMUTABLE_STATUS,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_DISABLED,
+        )
+    ),
+    SEARCH_MUTABLE_STATUS_DATA_QUERY(
+        SEARCH_MUTABLE_STATUS,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_DISABLED,
+        )
+    ),
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
index 02aed1c..21bc75a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
@@ -19,22 +19,22 @@
 import android.content.ContentProvider
 import android.content.ContentValues
 import android.content.Context
-import android.content.Intent
 import android.content.UriMatcher
 import android.content.pm.ProviderInfo
 import android.database.Cursor
 import android.database.MatrixCursor
 import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
 import android.util.Log
 import androidx.annotation.VisibleForTesting
-import com.android.settingslib.spa.framework.common.ColumnEnum
-import com.android.settingslib.spa.framework.common.QueryEnum
+import com.android.settingslib.spa.framework.common.EntryStatusData
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.common.addUri
-import com.android.settingslib.spa.framework.common.getColumns
 import com.android.settingslib.spa.framework.util.SESSION_SEARCH
 import com.android.settingslib.spa.framework.util.createIntent
+import com.android.settingslib.spa.slice.fromEntry
+
 
 private const val TAG = "SpaSearchProvider"
 
@@ -42,18 +42,25 @@
  * The content provider to return entry related data, which can be used for search and hierarchy.
  * One can query the provider result by:
  *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
- * For gallery, AuthorityPath = com.android.spa.gallery.provider
- * For Settings, AuthorityPath = com.android.settings.spa.provider
+ * For gallery, AuthorityPath = com.android.spa.gallery.search.provider
+ * For Settings, AuthorityPath = com.android.settings.spa.search.provider"
  * Some examples:
- *   $ adb shell content query --uri content://<AuthorityPath>/search_static
- *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
- *   $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_static_data
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic_data
  *   $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
  */
 class SpaSearchProvider : ContentProvider() {
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
     private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
 
+    private val queryMatchCode = mapOf(
+        SEARCH_STATIC_DATA to 301,
+        SEARCH_DYNAMIC_DATA to 302,
+        SEARCH_MUTABLE_STATUS to 303,
+        SEARCH_IMMUTABLE_STATUS to 304
+    )
+
     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
         TODO("Implement this to handle requests to delete one or more rows")
     }
@@ -85,10 +92,9 @@
 
     override fun attachInfo(context: Context?, info: ProviderInfo?) {
         if (info != null) {
-            QueryEnum.SEARCH_STATIC_DATA_QUERY.addUri(uriMatcher, info.authority)
-            QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.addUri(uriMatcher, info.authority)
-            QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority)
-            QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority)
+            for (entry in queryMatchCode) {
+                uriMatcher.addURI(info.authority, entry.key, entry.value)
+            }
         }
         super.attachInfo(context, info)
     }
@@ -102,11 +108,11 @@
     ): Cursor? {
         return try {
             when (uriMatcher.match(uri)) {
-                QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData()
-                QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData()
-                QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.queryMatchCode ->
+                queryMatchCode[SEARCH_STATIC_DATA] -> querySearchStaticData()
+                queryMatchCode[SEARCH_DYNAMIC_DATA] -> querySearchDynamicData()
+                queryMatchCode[SEARCH_MUTABLE_STATUS] ->
                     querySearchMutableStatusData()
-                QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.queryMatchCode ->
+                queryMatchCode[SEARCH_IMMUTABLE_STATUS] ->
                     querySearchImmutableStatusData()
                 else -> throw UnsupportedOperationException("Unknown Uri $uri")
             }
@@ -167,23 +173,45 @@
 
         // Fetch search data. We can add runtime arguments later if necessary
         val searchData = entry.getSearchData() ?: return
-        val intent = entry.createIntent(SESSION_SEARCH) ?: Intent()
-        cursor.newRow()
-            .add(ColumnEnum.ENTRY_ID.id, entry.id)
-            .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME))
+        val intent = entry.createIntent(SESSION_SEARCH)
+        val row = cursor.newRow().add(ColumnEnum.ENTRY_ID.id, entry.id)
             .add(ColumnEnum.SEARCH_TITLE.id, searchData.title)
             .add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword)
             .add(
                 ColumnEnum.SEARCH_PATH.id,
                 entryRepository.getEntryPathWithTitle(entry.id, searchData.title)
             )
+        intent?.let {
+            row.add(ColumnEnum.INTENT_TARGET_PACKAGE.id, spaEnvironment.appContext.packageName)
+                .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name)
+                .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras))
+        }
+        if (entry.hasSliceSupport)
+            row.add(
+                ColumnEnum.SLICE_URI.id, Uri.Builder()
+                    .fromEntry(entry, spaEnvironment.sliceProviderAuthorities)
+            )
+        // TODO: support legacy key
     }
 
     private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) {
         // Fetch status data. We can add runtime arguments later if necessary
-        val statusData = entry.getStatusData() ?: return
+        val statusData = entry.getStatusData() ?: EntryStatusData()
         cursor.newRow()
             .add(ColumnEnum.ENTRY_ID.id, entry.id)
-            .add(ColumnEnum.SEARCH_STATUS_DISABLED.id, statusData.isDisabled)
+            .add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled)
+    }
+
+    private fun QueryEnum.getColumns(): Array<String> {
+        return columnNames.map { it.id }.toTypedArray()
+    }
+
+    private fun marshall(parcelable: Parcelable?): ByteArray? {
+        if (parcelable == null) return null
+        val parcel = Parcel.obtain()
+        parcelable.writeToParcel(parcel, 0)
+        val bytes = parcel.marshall()
+        parcel.recycle()
+        return bytes
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 7db1ca1..ae88ed7 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -132,7 +132,10 @@
                     .asPaddingValues()
                     .horizontalValues()
             )
-            .padding(horizontal = SettingsDimension.itemPaddingAround),
+            .padding(
+                start = SettingsDimension.itemPaddingAround,
+                end = SettingsDimension.itemPaddingEnd,
+            ),
         overflow = TextOverflow.Ellipsis,
         maxLines = maxLines,
     )
diff --git a/packages/SettingsLib/Spa/tests/AndroidManifest.xml b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
index e2db594..1fda4e0 100644
--- a/packages/SettingsLib/Spa/tests/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
@@ -15,7 +15,7 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.settingslib.spa.tests">
+    package="com.android.settingslib.spa.test">
 
     <uses-sdk android:minSdkVersion="21"/>
 
@@ -26,6 +26,6 @@
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
         android:label="Tests for SpaLib"
-        android:targetPackage="com.android.settingslib.spa.tests">
+        android:targetPackage="com.android.settingslib.spa.test">
     </instrumentation>
 </manifest>
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
deleted file mode 100644
index 45f9b23..0000000
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-plugins {
-    id 'com.android.library'
-    id 'kotlin-android'
-}
-
-android {
-    namespace 'com.android.settingslib.spa.tests'
-    compileSdk TARGET_SDK
-    buildToolsVersion = BUILD_TOOLS_VERSION
-
-    defaultConfig {
-        minSdk MIN_SDK
-        targetSdk TARGET_SDK
-
-        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-    }
-
-    sourceSets {
-        main {
-            res.srcDirs = ["res"]
-        }
-        androidTest {
-            kotlin {
-                srcDir "src"
-            }
-            manifest.srcFile "AndroidManifest.xml"
-        }
-    }
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-    kotlinOptions {
-        jvmTarget = '1.8'
-        freeCompilerArgs = ["-Xjvm-default=all"]
-    }
-    buildFeatures {
-        compose true
-    }
-    composeOptions {
-        kotlinCompilerExtensionVersion jetpack_compose_compiler_version
-    }
-    buildTypes {
-        debug {
-            testCoverageEnabled = true
-        }
-    }
-}
-
-dependencies {
-    androidTestImplementation project(":spa")
-    androidTestImplementation project(":testutils")
-    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.28.1"
-}
-
-task coverageReport(type: JacocoReport, dependsOn: "connectedDebugAndroidTest") {
-    group = "Reporting"
-    description = "Generate Jacoco coverage reports after running tests."
-
-    sourceDirectories.from = files("../spa/src")
-    classDirectories.from = fileTree(
-            dir: "../spa/build/tmp/kotlin-classes/debug",
-            excludes: [
-                    "com/android/settingslib/spa/debug/**",
-
-                    // Excludes files forked from Accompanist.
-                    "com/android/settingslib/spa/framework/compose/DrawablePainter*",
-                    "com/android/settingslib/spa/framework/compose/Pager*",
-            ],
-    )
-    executionData.from = fileTree(dir: "$buildDir/outputs/code_coverage/debugAndroidTest/connected")
-}
diff --git a/packages/SettingsLib/Spa/tests/res/values/strings.xml b/packages/SettingsLib/Spa/tests/res/values/strings.xml
new file mode 100644
index 0000000..1ca425c
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/res/values/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <string name="test_quantity_strings">{count, plural,
+        =1    {There is one song found.}
+        other {There are # songs found.}
+    }</string>
+
+    <string name="test_quantity_strings_with_param">{count, plural,
+        =1    {There is one song found in {place}.}
+        other {There are # songs found in {place}.}
+    }</string>
+</resources>
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/MessageFormatsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/MessageFormatsTest.kt
new file mode 100644
index 0000000..2017ad1
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/MessageFormatsTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.util
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.test.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MessageFormatsTest {
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Test
+    fun formatString_one() {
+        val message = context.formatString(R.string.test_quantity_strings, "count" to 1)
+
+        assertThat(message).isEqualTo("There is one song found.")
+    }
+
+    @Test
+    fun formatString_other() {
+        val message = context.formatString(R.string.test_quantity_strings, "count" to 2)
+
+        assertThat(message).isEqualTo("There are 2 songs found.")
+    }
+
+    @Test
+    fun formatString_withParam_one() {
+        val message = context.formatString(
+            R.string.test_quantity_strings_with_param,
+            "count" to 1,
+            "place" to "phone",
+        )
+
+        assertThat(message).isEqualTo("There is one song found in phone.")
+    }
+
+    @Test
+    fun formatString_withParam_other() {
+        val message = context.formatString(
+            R.string.test_quantity_strings_with_param,
+            "count" to 2,
+            "place" to "phone",
+        )
+
+        assertThat(message).isEqualTo("There are 2 songs found in phone.")
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt
index cdb0f3a..831aded 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/search/SpaSearchProviderTest.kt
@@ -18,15 +18,14 @@
 
 import android.content.Context
 import android.database.Cursor
+import android.os.Bundle
+import android.os.Parcel
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.framework.common.ColumnEnum
-import com.android.settingslib.spa.framework.common.QueryEnum
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
-import com.android.settingslib.spa.framework.common.getIndex
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.android.settingslib.spa.tests.testutils.SppForSearch
 import com.google.common.truth.Truth
@@ -39,51 +38,145 @@
     private val spaEnvironment =
         SpaEnvironmentForTest(context, listOf(SppForSearch.createSettingsPage()))
     private val searchProvider = SpaSearchProvider()
+    private val pageOwner = spaEnvironment.createPage("SppForSearch")
 
     @Test
     fun testQuerySearchStatusData() {
         SpaEnvironmentFactory.reset(spaEnvironment)
-        val pageOwner = spaEnvironment.createPage("SppForSearch")
 
         val immutableStatus = searchProvider.querySearchImmutableStatusData()
-        Truth.assertThat(immutableStatus.count).isEqualTo(1)
+        Truth.assertThat(immutableStatus.count).isEqualTo(2)
         immutableStatus.moveToFirst()
         immutableStatus.checkValue(
             QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
             ColumnEnum.ENTRY_ID,
+            pageOwner.getEntryId("SearchStaticWithNoStatus")
+        )
+        immutableStatus.checkValue(
+            QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
+            ColumnEnum.ENTRY_DISABLED,
+            false.toString()
+        )
+
+        immutableStatus.moveToNext()
+        immutableStatus.checkValue(
+            QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
+            ColumnEnum.ENTRY_ID,
             pageOwner.getEntryId("SearchDynamicWithImmutableStatus")
         )
+        immutableStatus.checkValue(
+            QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, true.toString()
+        )
 
         val mutableStatus = searchProvider.querySearchMutableStatusData()
         Truth.assertThat(mutableStatus.count).isEqualTo(2)
         mutableStatus.moveToFirst()
         mutableStatus.checkValue(
-            QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
+            QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY,
             ColumnEnum.ENTRY_ID,
             pageOwner.getEntryId("SearchStaticWithMutableStatus")
         )
+        mutableStatus.checkValue(
+            QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, false.toString()
+        )
 
         mutableStatus.moveToNext()
         mutableStatus.checkValue(
-            QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY,
+            QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY,
             ColumnEnum.ENTRY_ID,
             pageOwner.getEntryId("SearchDynamicWithMutableStatus")
         )
+        mutableStatus.checkValue(
+            QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY, ColumnEnum.ENTRY_DISABLED, true.toString()
+        )
     }
 
     @Test
     fun testQuerySearchIndexData() {
         SpaEnvironmentFactory.reset(spaEnvironment)
+
         val staticData = searchProvider.querySearchStaticData()
         Truth.assertThat(staticData.count).isEqualTo(2)
+        staticData.moveToFirst()
+        staticData.checkValue(
+            QueryEnum.SEARCH_STATIC_DATA_QUERY,
+            ColumnEnum.ENTRY_ID,
+            pageOwner.getEntryId("SearchStaticWithNoStatus")
+        )
+        staticData.checkValue(
+            QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.SEARCH_TITLE, "SearchStaticWithNoStatus"
+        )
+        staticData.checkValue(
+            QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.SEARCH_KEYWORD, listOf("").toString()
+        )
+        staticData.checkValue(
+            QueryEnum.SEARCH_STATIC_DATA_QUERY,
+            ColumnEnum.SEARCH_PATH,
+            listOf("SearchStaticWithNoStatus", "SppForSearch").toString()
+        )
+        staticData.checkValue(
+            QueryEnum.SEARCH_STATIC_DATA_QUERY,
+            ColumnEnum.INTENT_TARGET_PACKAGE,
+            spaEnvironment.appContext.packageName
+        )
+        staticData.checkValue(
+            QueryEnum.SEARCH_STATIC_DATA_QUERY,
+            ColumnEnum.INTENT_TARGET_CLASS,
+            "com.android.settingslib.spa.tests.testutils.BlankActivity"
+        )
+
+        // Check extras in intent
+        val bundle =
+            staticData.getExtras(QueryEnum.SEARCH_STATIC_DATA_QUERY, ColumnEnum.INTENT_EXTRAS)
+        Truth.assertThat(bundle).isNotNull()
+        Truth.assertThat(bundle!!.size()).isEqualTo(3)
+        Truth.assertThat(bundle.getString("spaActivityDestination")).isEqualTo("SppForSearch")
+        Truth.assertThat(bundle.getString("highlightEntry"))
+            .isEqualTo(pageOwner.getEntryId("SearchStaticWithNoStatus"))
+        Truth.assertThat(bundle.getString("sessionSource")).isEqualTo("search")
+
+        staticData.moveToNext()
+        staticData.checkValue(
+            QueryEnum.SEARCH_STATIC_DATA_QUERY,
+            ColumnEnum.ENTRY_ID,
+            pageOwner.getEntryId("SearchStaticWithMutableStatus")
+        )
 
         val dynamicData = searchProvider.querySearchDynamicData()
         Truth.assertThat(dynamicData.count).isEqualTo(2)
+        dynamicData.moveToFirst()
+        dynamicData.checkValue(
+            QueryEnum.SEARCH_DYNAMIC_DATA_QUERY,
+            ColumnEnum.ENTRY_ID,
+            pageOwner.getEntryId("SearchDynamicWithMutableStatus")
+        )
+
+        dynamicData.moveToNext()
+        dynamicData.checkValue(
+            QueryEnum.SEARCH_DYNAMIC_DATA_QUERY,
+            ColumnEnum.ENTRY_ID,
+            pageOwner.getEntryId("SearchDynamicWithImmutableStatus")
+        )
+        dynamicData.checkValue(
+            QueryEnum.SEARCH_DYNAMIC_DATA_QUERY,
+            ColumnEnum.SEARCH_KEYWORD,
+            listOf("kw1", "kw2").toString()
+        )
     }
 }
 
 private fun Cursor.checkValue(query: QueryEnum, column: ColumnEnum, value: String) {
-    Truth.assertThat(getString(query.getIndex(column))).isEqualTo(value)
+    Truth.assertThat(getString(query.columnNames.indexOf(column))).isEqualTo(value)
+}
+
+private fun Cursor.getExtras(query: QueryEnum, column: ColumnEnum): Bundle? {
+    val extrasByte = getBlob(query.columnNames.indexOf(column)) ?: return null
+    val parcel = Parcel.obtain()
+    parcel.unmarshall(extrasByte, 0, extrasByte.size)
+    parcel.setDataPosition(0)
+    val bundle = Bundle()
+    bundle.readFromParcel(parcel)
+    return bundle
 }
 
 private fun SettingsPage.getEntryId(name: String): String {
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt
index 8101a94..f59b0de 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt
@@ -17,11 +17,13 @@
 package com.android.settingslib.spa.widget.button
 
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Close
 import androidx.compose.material.icons.outlined.Launch
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.getBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
@@ -66,4 +68,19 @@
 
         assertThat(clicked).isTrue()
     }
+
+    @Test
+    fun twoButtons_positionIsAligned() {
+        composeTestRule.setContent {
+            ActionButtons(
+                listOf(
+                    ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) {},
+                    ActionButton(text = "Close", imageVector = Icons.Outlined.Close) {},
+                )
+            )
+        }
+
+        assertThat(composeTestRule.onNodeWithText("Open").getBoundsInRoot().top)
+            .isEqualTo(composeTestRule.onNodeWithText("Close").getBoundsInRoot().top)
+    }
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/illustration/IllustrationTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/illustration/IllustrationTest.kt
index 77c505d..105fdc8 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/illustration/IllustrationTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/illustration/IllustrationTest.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.test.hasAnyAncestor
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.tests.R
+import com.android.settingslib.spa.test.R
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/FooterTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/FooterTest.kt
new file mode 100644
index 0000000..9d0501f
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/FooterTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.ui
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FooterTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun footer_isEmpty() {
+        composeTestRule.setContent {
+            Footer(footerText = "")
+        }
+
+        composeTestRule.onRoot().assertIsNotDisplayed()
+    }
+
+    @Test
+    fun footer_notEmpty_displayed() {
+        composeTestRule.setContent {
+            Footer(footerText = FOOTER_TEXT)
+        }
+
+        composeTestRule.onNodeWithText(FOOTER_TEXT).assertIsDisplayed()
+    }
+
+    private companion object {
+        const val FOOTER_TEXT = "Footer text"
+    }
+}
diff --git a/packages/SettingsLib/Spa/testutils/Android.bp b/packages/SettingsLib/Spa/testutils/Android.bp
index de87dde..2c1e1c2 100644
--- a/packages/SettingsLib/Spa/testutils/Android.bp
+++ b/packages/SettingsLib/Spa/testutils/Android.bp
@@ -24,7 +24,9 @@
     srcs: ["src/**/*.kt"],
 
     static_libs: [
+        "SpaLib",
         "androidx.arch.core_core-testing",
+        "androidx.compose.runtime_runtime",
         "androidx.compose.ui_ui-test-junit4",
         "androidx.compose.ui_ui-test-manifest",
         "mockito",
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle
index 81e54c1..dd7058d 100644
--- a/packages/SettingsLib/Spa/testutils/build.gradle
+++ b/packages/SettingsLib/Spa/testutils/build.gradle
@@ -44,9 +44,17 @@
         jvmTarget = '1.8'
         freeCompilerArgs = ["-Xjvm-default=all"]
     }
+    buildFeatures {
+        compose true
+    }
+    composeOptions {
+        kotlinCompilerExtensionVersion jetpack_compose_compiler_version
+    }
 }
 
 dependencies {
+    api project(":spa")
+
     api "androidx.arch.core:core-testing:2.1.0"
     api "androidx.compose.ui:ui-test-junit4:$jetpack_compose_version"
     api "com.google.truth:truth:1.1.3"
diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/FakeNavControllerWrapper.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/FakeNavControllerWrapper.kt
new file mode 100644
index 0000000..5a3044d
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/FakeNavControllerWrapper.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.testutils
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import com.android.settingslib.spa.framework.compose.LocalNavController
+import com.android.settingslib.spa.framework.compose.NavControllerWrapper
+
+class FakeNavControllerWrapper : NavControllerWrapper {
+    var navigateCalledWith: String? = null
+    var navigateBackIsCalled = false
+
+    override fun navigate(route: String) {
+        navigateCalledWith = route
+    }
+
+    override fun navigateBack() {
+        navigateBackIsCalled = true
+    }
+
+    @Composable
+    fun Wrapper(content: @Composable () -> Unit) {
+        CompositionLocalProvider(LocalNavController provides this) {
+            content()
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt
new file mode 100644
index 0000000..dddda55
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/LiveDataTestUtil.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.testutils
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+fun <T> LiveData<T>.getOrAwaitValue(
+    timeout: Long = 1,
+    timeUnit: TimeUnit = TimeUnit.SECONDS,
+    afterObserve: () -> Unit = {},
+): T? {
+    var data: T? = null
+    val latch = CountDownLatch(1)
+    val observer = Observer<T> { newData ->
+        data = newData
+        latch.countDown()
+    }
+    this.observeForever(observer)
+
+    afterObserve()
+
+    try {
+        // Don't wait indefinitely if the LiveData is not set.
+        if (!latch.await(timeout, timeUnit)) {
+            throw TimeoutException("LiveData value was never set.")
+        }
+    } finally {
+        this.removeObserver(observer)
+    }
+
+    return data
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index 487dbcb..4c144b2 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -29,7 +29,7 @@
 /**
  * The config used to load the App List.
  */
-internal data class AppListConfig(
+data class AppListConfig(
     val userId: Int,
     val showInstantApps: Boolean,
 )
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index 15766e1..2e0d853 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -49,13 +49,13 @@
 private const val TAG = "AppList"
 private const val CONTENT_TYPE_HEADER = "header"
 
-internal data class AppListState(
+data class AppListState(
     val showSystem: State<Boolean>,
     val option: State<Int>,
     val searchQuery: State<String>,
 )
 
-internal data class AppListInput<T : AppRecord>(
+data class AppListInput<T : AppRecord>(
     val config: AppListConfig,
     val listModel: AppListModel<T>,
     val state: AppListState,
@@ -70,10 +70,13 @@
  * This UI element will take the remaining space on the screen to show the App List.
  */
 @Composable
-internal fun <T : AppRecord> AppListInput<T>.AppList(
-    appListDataSupplier: @Composable () -> State<AppListData<T>?> = {
-        loadAppListData(config, listModel, state)
-    },
+fun <T : AppRecord> AppListInput<T>.AppList() {
+    AppListImpl { loadAppListData(config, listModel, state) }
+}
+
+@Composable
+internal fun <T : AppRecord> AppListInput<T>.AppListImpl(
+    appListDataSupplier: @Composable () -> State<AppListData<T>?>,
 ) {
     LogCompositions(TAG, config.userId.toString())
     val appListData = appListDataSupplier()
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt
index 28bf832..6d0d7d6 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt
@@ -28,7 +28,7 @@
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spaprivileged.model.app.AppRecord
 
-class AppListItemModel<T : AppRecord>(
+data class AppListItemModel<T : AppRecord>(
     val record: T,
     val label: String,
     val summary: State<String>,
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
index d452c74..cb35fb0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
@@ -47,24 +47,9 @@
     primaryUserOnly: Boolean = false,
     moreOptions: @Composable MoreOptionsScope.() -> Unit = {},
     header: @Composable () -> Unit = {},
+    appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
     appItem: @Composable AppListItemModel<T>.() -> Unit,
 ) {
-    AppListPageImpl(
-        title, listModel, showInstantApps, primaryUserOnly, moreOptions, header, appItem,
-    ) { it.AppList() }
-}
-
-@Composable
-internal fun <T : AppRecord> AppListPageImpl(
-    title: String,
-    listModel: AppListModel<T>,
-    showInstantApps: Boolean = false,
-    primaryUserOnly: Boolean = false,
-    moreOptions: @Composable MoreOptionsScope.() -> Unit = {},
-    header: @Composable () -> Unit = {},
-    appItem: @Composable AppListItemModel<T>.() -> Unit,
-    appList: @Composable (input: AppListInput<T>) -> Unit,
-) {
     val showSystem = rememberSaveable { mutableStateOf(false) }
     SearchScaffold(
         title = title,
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
new file mode 100644
index 0000000..01f4cc6
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.compose
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class DisposableBroadcastReceiverAsUserTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @get:Rule
+    val mockito: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var context: Context
+
+    private var registeredBroadcastReceiver: BroadcastReceiver? = null
+
+    @Before
+    fun setUp() {
+        whenever(context.registerReceiverAsUser(any(), any(), any(), any(), any()))
+            .thenAnswer {
+                registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver
+                null
+            }
+    }
+
+    @Test
+    fun broadcastReceiver_registered() {
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                DisposableBroadcastReceiverAsUser(IntentFilter(), USER_HANDLE) {}
+            }
+        }
+
+        assertThat(registeredBroadcastReceiver).isNotNull()
+    }
+
+    @Test
+    fun broadcastReceiver_isCalledOnReceive() {
+        var onReceiveIsCalled = false
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                DisposableBroadcastReceiverAsUser(IntentFilter(), USER_HANDLE) {
+                    onReceiveIsCalled = true
+                }
+            }
+        }
+
+        registeredBroadcastReceiver!!.onReceive(context, Intent())
+
+        assertThat(onReceiveIsCalled).isTrue()
+    }
+
+    @Test
+    fun broadcastReceiver_onStartIsCalled() {
+        var onStartIsCalled = false
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                DisposableBroadcastReceiverAsUser(
+                    intentFilter = IntentFilter(),
+                    userHandle = USER_HANDLE,
+                    onStart = { onStartIsCalled = true },
+                    onReceive = {},
+                )
+            }
+        }
+
+        assertThat(onStartIsCalled).isTrue()
+    }
+
+    private companion object {
+        val USER_HANDLE: UserHandle = UserHandle.of(0)
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt
index 946dc2a..f2267f6 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt
@@ -118,12 +118,12 @@
     ): State<AppListInput<TestAppRecord>?> {
         val appListState = mutableStateOf<AppListInput<TestAppRecord>?>(null)
         composeTestRule.setContent {
-            AppListPageImpl(
+            AppListPage(
                 title = TITLE,
                 listModel = TestAppListModel(options),
                 header = header,
                 appItem = { AppListItem {} },
-                appList = { appListState.value = it },
+                appList = { appListState.value = this },
             )
         }
         return appListState
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt
new file mode 100644
index 0000000..abdcd3b
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.template.app
+
+import android.content.pm.ApplicationInfo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.isToggleable
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AppListSwitchItemTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun appLabel_displayed() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = {},
+                checked = stateOf(null),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
+    }
+
+    @Test
+    fun summary_displayed() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = {},
+                checked = stateOf(null),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNodeWithText(SUMMARY).assertIsDisplayed()
+    }
+
+    @Test
+    fun title_onClick() {
+        var titleClicked = false
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = { titleClicked = true },
+                checked = stateOf(false),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+
+        assertThat(titleClicked).isTrue()
+    }
+
+    @Test
+    fun switch_checkIsNull() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = {},
+                checked = stateOf(null),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertDoesNotExist()
+    }
+
+    @Test
+    fun switch_checked() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = {},
+                checked = stateOf(true),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertIsOn()
+    }
+
+    @Test
+    fun switch_notChecked() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = {},
+                checked = stateOf(false),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertIsOff()
+    }
+
+    @Test
+    fun switch_changeable() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = {},
+                checked = stateOf(false),
+                changeable = stateOf(true),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertIsEnabled()
+    }
+
+    @Test
+    fun switch_notChangeable() {
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = {},
+                checked = stateOf(false),
+                changeable = stateOf(false),
+                onCheckedChange = {},
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).assertIsNotEnabled()
+    }
+
+    @Test
+    fun switch_onClick() {
+        var switchClicked = false
+        composeTestRule.setContent {
+            ITEM_MODEL.AppListSwitchItem(
+                onClick = {},
+                checked = stateOf(false),
+                changeable = stateOf(true),
+                onCheckedChange = { switchClicked = true },
+            )
+        }
+
+        composeTestRule.onNode(isToggleable()).performClick()
+
+        assertThat(switchClicked).isTrue()
+    }
+
+    private companion object {
+        const val PACKAGE_NAME = "package.name"
+        const val LABEL = "Label"
+        const val SUMMARY = "Summary"
+        val APP = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+        }
+        val ITEM_MODEL = AppListItemModel(
+            record = object : AppRecord {
+                override val app = APP
+            },
+            label = LABEL,
+            summary = stateOf(SUMMARY),
+        )
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt
index 945bd51..2677669 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt
@@ -102,7 +102,7 @@
                 appItem = { AppListItem {} },
                 bottomPadding = 0.dp,
             )
-            appListInput.AppList { stateOf(AppListData(appEntries, option = 0)) }
+            appListInput.AppListImpl { stateOf(AppListData(appEntries, option = 0)) }
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 61c7fb9..2951001 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -163,6 +163,11 @@
         mUnpairing = false;
     }
 
+    /** Clears any pending messages in the message queue. */
+    public void release() {
+        mHandler.removeCallbacksAndMessages(null);
+    }
+
     private void initDrawableCache() {
         int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
         int cacheSize = maxMemory / 8;
@@ -1441,11 +1446,13 @@
         final boolean tmpJustDiscovered = mJustDiscovered;
         final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo;
         // Set main device from sub device
+        release();
         mDevice = mSubDevice.mDevice;
         mRssi = mSubDevice.mRssi;
         mJustDiscovered = mSubDevice.mJustDiscovered;
         mHearingAidInfo = mSubDevice.mHearingAidInfo;
         // Set sub device from backup
+        mSubDevice.release();
         mSubDevice.mDevice = tmpDevice;
         mSubDevice.mRssi = tmpRssi;
         mSubDevice.mJustDiscovered = tmpJustDiscovered;
@@ -1471,6 +1478,7 @@
      * Remove a device from the member device sets.
      */
     public void removeMemberDevice(CachedBluetoothDevice memberDevice) {
+        memberDevice.release();
         mMemberDevices.remove(memberDevice);
     }
 
@@ -1488,11 +1496,13 @@
         final short tmpRssi = mRssi;
         final boolean tmpJustDiscovered = mJustDiscovered;
         // Set main device from sub device
+        release();
         mDevice = newMainDevice.mDevice;
         mRssi = newMainDevice.mRssi;
         mJustDiscovered = newMainDevice.mJustDiscovered;
 
         // Set sub device from backup
+        newMainDevice.release();
         newMainDevice.mDevice = tmpDevice;
         newMainDevice.mRssi = tmpRssi;
         newMainDevice.mJustDiscovered = tmpJustDiscovered;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index dd56bde..221836b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -223,8 +223,14 @@
 
     public synchronized void clearNonBondedDevices() {
         clearNonBondedSubDevices();
-        mCachedDevices.removeIf(cachedDevice
-            -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE);
+        final List<CachedBluetoothDevice> removedCachedDevice = new ArrayList<>();
+        mCachedDevices.stream()
+                .filter(cachedDevice -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE)
+                .forEach(cachedDevice -> {
+                    cachedDevice.release();
+                    removedCachedDevice.add(cachedDevice);
+                });
+        mCachedDevices.removeAll(removedCachedDevice);
     }
 
     private void clearNonBondedSubDevices() {
@@ -245,6 +251,7 @@
             if (subDevice != null
                     && subDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
                 // Sub device exists and it is not bonded
+                subDevice.release();
                 cachedDevice.setSubDevice(null);
             }
         }
@@ -294,6 +301,7 @@
                 }
                 if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
                     cachedDevice.setJustDiscovered(false);
+                    cachedDevice.release();
                     mCachedDevices.remove(i);
                 }
             }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
index f06aab3..6b7f733 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
@@ -176,11 +176,11 @@
      * @param device is the device for which we want to know if supports synchronized presets
      * @return {@code true} if the device supports synchronized presets
      */
-    public boolean supportSynchronizedPresets(@NonNull BluetoothDevice device) {
+    public boolean supportsSynchronizedPresets(@NonNull BluetoothDevice device) {
         if (mService == null) {
             return false;
         }
-        return mService.supportSynchronizedPresets(device);
+        return mService.supportsSynchronizedPresets(device);
     }
 
     /**
@@ -189,11 +189,11 @@
      * @param device is the device for which we want to know if supports independent presets
      * @return {@code true} if the device supports independent presets
      */
-    public boolean supportIndependentPresets(@NonNull BluetoothDevice device) {
+    public boolean supportsIndependentPresets(@NonNull BluetoothDevice device) {
         if (mService == null) {
             return false;
         }
-        return mService.supportIndependentPresets(device);
+        return mService.supportsIndependentPresets(device);
     }
 
     /**
@@ -202,11 +202,11 @@
      * @param device is the device for which we want to know if supports dynamic presets
      * @return {@code true} if the device supports dynamic presets
      */
-    public boolean supportDynamicPresets(@NonNull BluetoothDevice device) {
+    public boolean supportsDynamicPresets(@NonNull BluetoothDevice device) {
         if (mService == null) {
             return false;
         }
-        return mService.supportDynamicPresets(device);
+        return mService.supportsDynamicPresets(device);
     }
 
     /**
@@ -215,11 +215,11 @@
      * @param device is the device for which we want to know if supports writable presets
      * @return {@code true} if the device supports writable presets
      */
-    public boolean supportWritablePresets(@NonNull BluetoothDevice device) {
+    public boolean supportsWritablePresets(@NonNull BluetoothDevice device) {
         if (mService == null) {
             return false;
         }
-        return mService.supportWritablePresets(device);
+        return mService.supportsWritablePresets(device);
     }
 
     @Override
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 65671a2..77c19a1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -998,10 +998,12 @@
 
         mCachedDevice.switchSubDeviceContent();
 
+        verify(mCachedDevice).release();
         assertThat(mCachedDevice.mRssi).isEqualTo(RSSI_2);
         assertThat(mCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
         assertThat(mCachedDevice.mDevice).isEqualTo(mSubDevice);
         assertThat(mCachedDevice.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_RIGHT);
+        verify(mSubCachedDevice).release();
         assertThat(mSubCachedDevice.mRssi).isEqualTo(RSSI_1);
         assertThat(mSubCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
         assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 0b7b2f9..9192086 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -1144,7 +1144,7 @@
             Slog.v(LOG_TAG, "getConfigSetting(" + name + ")");
         }
 
-        DeviceConfig.enforceReadPermission(/*namespace=*/name.split("/")[0]);
+        Settings.Config.enforceReadPermission(/*namespace=*/name.split("/")[0]);
 
         // Get the value.
         synchronized (mLock) {
@@ -1317,7 +1317,7 @@
             Slog.v(LOG_TAG, "getAllConfigFlags() for " + prefix);
         }
 
-        DeviceConfig.enforceReadPermission(
+        Settings.Config.enforceReadPermission(
                 prefix != null ? prefix.split("/")[0] : null);
 
         synchronized (mLock) {
@@ -3595,8 +3595,8 @@
 
         private Uri getNotificationUriFor(int key, String name) {
             if (isConfigSettingsKey(key)) {
-                return (name != null) ? Uri.withAppendedPath(DeviceConfig.CONTENT_URI, name)
-                        : DeviceConfig.CONTENT_URI;
+                return (name != null) ? Uri.withAppendedPath(Settings.Config.CONTENT_URI, name)
+                        : Settings.Config.CONTENT_URI;
             } else if (isGlobalSettingsKey(key)) {
                 return (name != null) ? Uri.withAppendedPath(Settings.Global.CONTENT_URI, name)
                         : Settings.Global.CONTENT_URI;
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index aa194b9..153f0b4 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -772,6 +772,7 @@
                  Settings.Secure.SLEEP_TIMEOUT,
                  Settings.Secure.SMS_DEFAULT_APPLICATION,
                  Settings.Secure.SPELL_CHECKER_ENABLED,  // Intentionally removed in Q
+                 Settings.Secure.STYLUS_BUTTONS_DISABLED,
                  Settings.Secure.TRUST_AGENTS_INITIALIZED,
                  Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED,
                  Settings.Secure.TV_APP_USES_NON_SYSTEM_INPUTS,
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
index e588b3d..753378b 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
@@ -22,7 +22,6 @@
 
 import android.content.ContentResolver;
 import android.os.Bundle;
-import android.provider.DeviceConfig;
 import android.provider.Settings;
 
 import androidx.test.InstrumentationRegistry;
@@ -180,14 +179,14 @@
             args.putBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY, true);
         }
         resolver.call(
-                DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, compositeName, args);
+                Settings.Config.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, compositeName, args);
     }
 
     private static String getFromContentProvider(ContentResolver resolver, String namespace,
             String key) {
         String compositeName = namespace + "/" + key;
         Bundle result = resolver.call(
-                DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, compositeName, null);
+                Settings.Config.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, compositeName, null);
         assertNotNull(result);
         return result.getString(Settings.NameValueTable.VALUE);
     }
@@ -196,7 +195,8 @@
             String key) {
         String compositeName = namespace + "/" + key;
         Bundle result = resolver.call(
-                DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, compositeName, null);
+                Settings.Config.CONTENT_URI,
+                Settings.CALL_METHOD_DELETE_CONFIG, compositeName, null);
         assertNotNull(result);
         return compositeName.equals(result.getString(Settings.NameValueTable.VALUE));
     }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 680a0a1..d3ba5e6 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -754,6 +754,9 @@
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
 
     <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
 
     <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 7db68b0..a8216e8 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -914,6 +914,29 @@
         <service android:name=".controls.controller.AuxiliaryPersistenceWrapper$DeletionJobService"
                  android:permission="android.permission.BIND_JOB_SERVICE"/>
 
+        <!-- region Note Task -->
+        <activity
+            android:name=".notetask.shortcut.CreateNoteTaskShortcutActivity"
+            android:enabled="false"
+            android:exported="true"
+            android:excludeFromRecents="true"
+            android:theme="@android:style/Theme.NoDisplay"
+            android:label="@string/note_task_button_label"
+            android:icon="@drawable/ic_note_task_button">
+
+            <intent-filter>
+                <action android:name="android.intent.action.CREATE_SHORTCUT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name=".notetask.shortcut.LaunchNoteTaskActivity"
+            android:exported="true"
+            android:excludeFromRecents="true"
+            android:theme="@android:style/Theme.NoDisplay" />
+        <!-- endregion -->
+
         <!-- started from ControlsRequestReceiver -->
         <activity
             android:name=".controls.management.ControlsRequestDialog"
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 17ad55f..8acc2f8 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -44,23 +44,3 @@
     manifest: "AndroidManifest.xml",
     kotlincflags: ["-Xjvm-default=all"],
 }
-
-android_test {
-    name: "SystemUIAnimationLibTests",
-
-    static_libs: [
-        "SystemUIAnimationLib",
-        "androidx.test.ext.junit",
-        "androidx.test.rules",
-        "testables",
-    ],
-    libs: [
-        "android.test.base",
-    ],
-    srcs: [
-        "**/*.java",
-        "**/*.kt",
-    ],
-    kotlincflags: ["-Xjvm-default=all"],
-    test_suites: ["general-tests"],
-}
diff --git a/packages/SystemUI/animation/TEST_MAPPING b/packages/SystemUI/animation/TEST_MAPPING
deleted file mode 100644
index 3dc8510..0000000
--- a/packages/SystemUI/animation/TEST_MAPPING
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "SystemUIAnimationLibTests"
-    }
-  ]
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 22944b8..462b90a 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -237,6 +237,28 @@
         this.lockScreenColor = lockScreenColor
     }
 
+    fun animateColorChange() {
+        logBuffer?.log(tag, DEBUG, "animateColorChange")
+        setTextStyle(
+            weight = lockScreenWeight,
+            textSize = -1f,
+            color = null, /* using current color */
+            animate = false,
+            duration = 0,
+            delay = 0,
+            onAnimationEnd = null
+        )
+        setTextStyle(
+            weight = lockScreenWeight,
+            textSize = -1f,
+            color = lockScreenColor,
+            animate = true,
+            duration = COLOR_ANIM_DURATION,
+            delay = 0,
+            onAnimationEnd = null
+        )
+    }
+
     fun animateAppearOnLockscreen() {
         logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
         setTextStyle(
@@ -350,6 +372,7 @@
      *
      * By passing -1 to weight, the view preserves its current weight.
      * By passing -1 to textSize, the view preserves its current text size.
+     * By passing null to color, the view preserves its current color.
      *
      * @param weight text weight.
      * @param textSize font size.
@@ -611,6 +634,7 @@
         private const val APPEAR_ANIM_DURATION: Long = 350
         private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
         private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
+        private const val COLOR_ANIM_DURATION: Long = 400
 
         // Constants for the animation
         private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index e1f2174..c540f0f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -142,7 +142,7 @@
             currentColor = color
             view.setColors(DOZE_COLOR, color)
             if (!animations.dozeState.isActive) {
-                view.animateAppearOnLockscreen()
+                view.animateColorChange()
             }
         }
     }
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
index 898935f..2cac9c7 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
@@ -21,8 +21,6 @@
     android:id="@+id/keyguard_bouncer_user_switcher"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:clipChildren="false"
-    android:clipToPadding="false"
     android:orientation="vertical"
     android:gravity="center"
     android:importantForAccessibility="yes"> <!-- Needed because TYPE_WINDOW_STATE_CHANGED is sent
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml b/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
index 411fea5..316ad39 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
@@ -18,6 +18,8 @@
     <TextView
         android:id="@+id/digit_text"
         style="@style/Widget.TextView.NumPadKey.Digit"
+        android:autoSizeMaxTextSize="32sp"
+        android:autoSizeTextType="uniform"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         />
diff --git a/packages/SystemUI/res/drawable/ic_note_task_button.xml b/packages/SystemUI/res/drawable/ic_note_task_button.xml
new file mode 100644
index 0000000..bb5e224
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_note_task_button.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="#636C6F"
+        android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z" />
+    <path
+        android:fillColor="#636C6F"
+        android:fillType="evenOdd"
+        android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z" />
+</vector>
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 530db0d..13c9a5e 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -35,7 +35,6 @@
         android:layout_height="@dimen/qs_media_session_height_expanded"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         android:translationZ="0dp"
         android:scaleType="centerCrop"
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index 80628f9..f4434e8 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -36,7 +36,4 @@
     <integer name="qs_security_footer_maxLines">1</integer>
 
     <bool name="config_use_large_screen_shade_header">true</bool>
-
-    <!-- Whether to show the side fps hint while on bouncer -->
-    <bool name="config_show_sidefps_hint_on_bouncer">true</bool>
 </resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index b8e2caf..247e44d 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -555,7 +555,7 @@
     <string name="config_preferredEmergencySosPackage" translatable="false"></string>
 
     <!-- Whether to show the side fps hint while on bouncer -->
-    <bool name="config_show_sidefps_hint_on_bouncer">false</bool>
+    <bool name="config_show_sidefps_hint_on_bouncer">true</bool>
 
     <!-- Whether to use the split 2-column notification shade -->
     <bool name="config_use_split_notification_shade">false</bool>
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index 49dd574..fd2e324 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -36,4 +36,8 @@
       avatar will no longer show on the lockscreen -->
     <bool name="flag_user_switcher_chip">false</bool>
 
+    <!-- Whether the battery icon is allowed to display a shield when battery life is being
+         protected. -->
+    <bool name="flag_battery_shield_icon">false</bool>
+
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index a4e9983..9ee918d 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2773,6 +2773,10 @@
         <xliff:g id="weather_condition" example="Partly cloudy">%1$s</xliff:g>, <xliff:g id="temperature" example="7°C">%2$s</xliff:g>
     </string>
 
+    <!-- TODO(b/259369672): Replace with final resource. -->
+    <!-- [CHAR LIMIT=30] Label used to open Note Task -->
+    <string name="note_task_button_label">Notetaking</string>
+
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is broadcasting -->
     <string name="broadcasting_description_is_broadcasting">Broadcasting</string>
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, title -->
diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
index de855e2..c32de70 100644
--- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml
+++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
@@ -124,20 +124,9 @@
         </KeyFrameSet>
     </Transition>
 
-    <Transition
-        android:id="@+id/large_screen_header_transition"
-        app:constraintSetStart="@id/large_screen_header_constraint"
-        app:constraintSetEnd="@id/large_screen_header_constraint"/>
+    <Include app:constraintSet="@xml/large_screen_shade_header"/>
 
-    <!--
-        Placeholder ConstraintSet. They are populated in the controller for this class.
-        This is needed because there's no easy way to just refer to a `ConstraintSet` file. The
-        options are either a layout file or inline the ConstraintSets.
-     -->
-    <ConstraintSet android:id="@id/qqs_header_constraint"/>
+    <Include app:constraintSet="@xml/qs_header"/>
 
-    <ConstraintSet android:id="@id/qs_header_constraint"/>
-
-    <ConstraintSet android:id="@id/large_screen_header_constraint" />
-
+    <Include app:constraintSet="@xml/qqs_header"/>
 </MotionScene>
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index 5d3650c..e56e5d5 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -59,7 +59,6 @@
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
-            app:layout_constrainedWidth="true"
             app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/date"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
@@ -75,7 +74,6 @@
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
-            app:layout_constrainedWidth="true"
             app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="@id/end_guide"
@@ -112,5 +110,4 @@
             app:layout_constraintHorizontal_bias="1"
         />
     </Constraint>
-
 </ConstraintSet>
\ No newline at end of file
diff --git a/packages/SystemUI/res/xml/qs_header.xml b/packages/SystemUI/res/xml/qs_header.xml
index 982c422..eca2b2a 100644
--- a/packages/SystemUI/res/xml/qs_header.xml
+++ b/packages/SystemUI/res/xml/qs_header.xml
@@ -56,6 +56,7 @@
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            app:layout_constrainedWidth="true"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toStartOf="@id/space"
             app:layout_constraintBottom_toBottomOf="parent"
@@ -88,7 +89,6 @@
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
-            app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/space"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
             app:layout_constraintTop_toTopOf="@id/date"
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 2cc5ccdc..c985fd7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -34,6 +34,7 @@
 import android.graphics.Rect;
 import android.os.Trace;
 import android.util.AttributeSet;
+import android.view.WindowInsets;
 import android.view.WindowInsetsAnimationControlListener;
 import android.view.WindowInsetsAnimationController;
 import android.view.animation.AnimationUtils;
@@ -236,4 +237,50 @@
         return getResources().getString(
                 com.android.internal.R.string.keyguard_accessibility_password_unlock);
     }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        if (!mPasswordEntry.isFocused() && isVisibleToUser()) {
+            mPasswordEntry.requestFocus();
+        }
+        return super.onApplyWindowInsets(insets);
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (hasWindowFocus) {
+            if (isVisibleToUser()) {
+                showKeyboard();
+            } else {
+                hideKeyboard();
+            }
+        }
+    }
+
+    /**
+     * Sends signal to the focused window to show the keyboard.
+     */
+    public void showKeyboard() {
+        post(() -> {
+            if (mPasswordEntry.isAttachedToWindow()
+                    && !mPasswordEntry.getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
+                mPasswordEntry.requestFocus();
+                mPasswordEntry.getWindowInsetsController().show(WindowInsets.Type.ime());
+            }
+        });
+    }
+
+    /**
+     * Sends signal to the focused window to hide the keyboard.
+     */
+    public void hideKeyboard() {
+        post(() -> {
+            if (mPasswordEntry.isAttachedToWindow()
+                    && mPasswordEntry.getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
+                mPasswordEntry.clearFocus();
+                mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime());
+            }
+        });
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 195e8f9..d221e22 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -26,7 +26,6 @@
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup.MarginLayoutParams;
-import android.view.WindowInsets;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodManager;
@@ -200,12 +199,9 @@
             return;
         }
 
-        mView.post(() -> {
-            if (mView.isShown()) {
-                mPasswordEntry.requestFocus();
-                mPasswordEntry.getWindowInsetsController().show(WindowInsets.Type.ime());
-            }
-        });
+        if (mView.isShown()) {
+            mView.showKeyboard();
+        }
     }
 
     @Override
@@ -227,16 +223,12 @@
                 super.onPause();
             });
         }
-        if (mPasswordEntry.isAttachedToWindow()) {
-            mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime());
-        }
+        mView.hideKeyboard();
     }
 
     @Override
     public void onStartingToHide() {
-        if (mPasswordEntry.isAttachedToWindow()) {
-            mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime());
-        }
+        mView.hideKeyboard();
     }
 
     private void updateSwitchImeButton() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 5c4126e..8f3484a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -221,10 +221,11 @@
                 public void onEnd(WindowInsetsAnimation animation) {
                     if (!mDisappearAnimRunning) {
                         endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_APPEAR);
-                        updateChildren(0 /* translationY */, 1f /* alpha */);
                     } else {
                         endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR);
+                        setAlpha(0f);
                     }
+                    updateChildren(0 /* translationY */, 1f /* alpha */);
                 }
 
                 private void updateChildren(int translationY, float alpha) {
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 02a6d7b..e6f559b 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -210,8 +210,10 @@
                 (FaceScanningOverlay) getOverlayView(mFaceScanningViewId);
         if (faceScanningOverlay != null) {
             faceScanningOverlay.setHideOverlayRunnable(() -> {
+                Trace.beginSection("ScreenDecorations#hideOverlayRunnable");
                 updateOverlayWindowVisibilityIfViewExists(
                         faceScanningOverlay.findViewById(mFaceScanningViewId));
+                Trace.endSection();
             });
             faceScanningOverlay.enableShowProtection(false);
         }
@@ -273,16 +275,18 @@
             if (mOverlays == null || !shouldOptimizeVisibility()) {
                 return;
             }
-
+            Trace.beginSection("ScreenDecorations#updateOverlayWindowVisibilityIfViewExists");
             for (final OverlayWindow overlay : mOverlays) {
                 if (overlay == null) {
                     continue;
                 }
                 if (overlay.getView(view.getId()) != null) {
                     overlay.getRootView().setVisibility(getWindowVisibility(overlay, true));
+                    Trace.endSection();
                     return;
                 }
             }
+            Trace.endSection();
         });
     }
 
@@ -370,6 +374,7 @@
     }
 
     private void startOnScreenDecorationsThread() {
+        Trace.beginSection("ScreenDecorations#startOnScreenDecorationsThread");
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mContext.getDisplay().getDisplayInfo(mDisplayInfo);
@@ -472,6 +477,7 @@
 
         mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
         updateConfiguration();
+        Trace.endSection();
     }
 
     @VisibleForTesting
@@ -521,6 +527,12 @@
     }
 
     private void setupDecorations() {
+        Trace.beginSection("ScreenDecorations#setupDecorations");
+        setupDecorationsInner();
+        Trace.endSection();
+    }
+
+    private void setupDecorationsInner() {
         if (hasRoundedCorners() || shouldDrawCutout() || isPrivacyDotEnabled()
                 || mFaceScanningFactory.getHasProviders()) {
 
@@ -573,7 +585,11 @@
                 return;
             }
 
-            mMainExecutor.execute(() -> mTunerService.addTunable(this, SIZE));
+            mMainExecutor.execute(() -> {
+                Trace.beginSection("ScreenDecorations#addTunable");
+                mTunerService.addTunable(this, SIZE);
+                Trace.endSection();
+            });
 
             // Watch color inversion and invert the overlay as needed.
             if (mColorInversionSetting == null) {
@@ -593,7 +609,11 @@
             mUserTracker.addCallback(mUserChangedCallback, mExecutor);
             mIsRegistered = true;
         } else {
-            mMainExecutor.execute(() -> mTunerService.removeTunable(this));
+            mMainExecutor.execute(() -> {
+                Trace.beginSection("ScreenDecorations#removeTunable");
+                mTunerService.removeTunable(this);
+                Trace.endSection();
+            });
 
             if (mColorInversionSetting != null) {
                 mColorInversionSetting.setListening(false);
@@ -939,6 +959,7 @@
         }
 
         mExecutor.execute(() -> {
+            Trace.beginSection("ScreenDecorations#onConfigurationChanged");
             int oldRotation = mRotation;
             mPendingConfigChange = false;
             updateConfiguration();
@@ -951,6 +972,7 @@
                 // the updated rotation).
                 updateLayoutParams();
             }
+            Trace.endSection();
         });
     }
 
@@ -1119,6 +1141,7 @@
             if (mOverlays == null || !SIZE.equals(key)) {
                 return;
             }
+            Trace.beginSection("ScreenDecorations#onTuningChanged");
             try {
                 final int sizeFactor = Integer.parseInt(newValue);
                 mRoundedCornerResDelegate.setTuningSizeFactor(sizeFactor);
@@ -1132,6 +1155,7 @@
                     R.id.rounded_corner_bottom_right
             });
             updateHwLayerRoundedCornerExistAndSize();
+            Trace.endSection();
         });
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 4c8e1ac..a07c716 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -28,6 +28,7 @@
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
 import android.service.controls.Control
+import android.service.controls.ControlsProviderService
 import android.util.Log
 import android.view.ContextThemeWrapper
 import android.view.LayoutInflater
@@ -48,6 +49,7 @@
 import com.android.systemui.R
 import com.android.systemui.controls.ControlsMetricsLogger
 import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.ControlsSettingsRepository
 import com.android.systemui.controls.CustomIconCache
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.controller.StructureInfo
@@ -96,6 +98,7 @@
         private val userFileManager: UserFileManager,
         private val userTracker: UserTracker,
         private val taskViewFactory: Optional<TaskViewFactory>,
+        private val controlsSettingsRepository: ControlsSettingsRepository,
         dumpManager: DumpManager
 ) : ControlsUiController, Dumpable {
 
@@ -354,7 +357,6 @@
                 } else {
                     items[0]
                 }
-
         maybeUpdateSelectedItem(selectionItem)
 
         createControlsSpaceFrame()
@@ -374,11 +376,20 @@
     }
 
     private fun createPanelView(componentName: ComponentName) {
-        val pendingIntent = PendingIntent.getActivity(
+        val setting = controlsSettingsRepository
+                .allowActionOnTrivialControlsInLockscreen.value
+        val pendingIntent = PendingIntent.getActivityAsUser(
                 context,
                 0,
-                Intent().setComponent(componentName),
-                PendingIntent.FLAG_IMMUTABLE
+                Intent()
+                        .setComponent(componentName)
+                        .putExtra(
+                                ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+                                setting
+                        ),
+                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+                null,
+                userTracker.userHandle
         )
 
         parent.requireViewById<View>(R.id.controls_scroll_view).visibility = View.GONE
@@ -698,6 +709,8 @@
             println("hidden: $hidden")
             println("selectedItem: $selectedItem")
             println("lastSelections: $lastSelections")
+            println("setting: ${controlsSettingsRepository
+                    .allowActionOnTrivialControlsInLockscreen.value}")
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 8019b56..ae2e1d1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -83,8 +83,7 @@
     val SEMI_STABLE_SORT = unreleasedFlag(115, "semi_stable_sort", teamfood = true)
 
     @JvmField
-    val NOTIFICATION_GROUP_CORNER =
-        unreleasedFlag(116, "notification_group_corner", teamfood = true)
+    val USE_ROUNDNESS_SOURCETYPES = unreleasedFlag(116, "use_roundness_sourcetype", teamfood = true)
 
     // TODO(b/259217907)
     @JvmField
@@ -227,7 +226,9 @@
     val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
 
     // TODO(b/256623670): Tracking Bug
-    @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon")
+    @JvmField
+    val BATTERY_SHIELD_ICON =
+        resourceBooleanFlag(610, R.bool.flag_battery_shield_icon, "battery_shield_icon")
 
     // TODO(b/260881289): Tracking Bug
     val NEW_STATUS_BAR_ICONS_DEBUG_COLORING =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 948239a..36c939d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2209,6 +2209,9 @@
                 case START_KEYGUARD_EXIT_ANIM:
                     Trace.beginSection(
                             "KeyguardViewMediator#handleMessage START_KEYGUARD_EXIT_ANIM");
+                    synchronized (KeyguardViewMediator.this) {
+                        mHiding = true;
+                    }
                     StartKeyguardExitAnimParams params = (StartKeyguardExitAnimParams) msg.obj;
                     mNotificationShadeWindowControllerLazy.get().batchApplyWindowLayoutParams(
                             () -> {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 827ac78..df8fb91 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -940,19 +940,9 @@
         if (mIsSeekBarEnabled) {
             return ConstraintSet.VISIBLE;
         }
-        // If disabled and "neighbours" are visible, set progress bar to INVISIBLE instead of GONE
-        // so layout weights still work.
-        return areAnyExpandedBottomActionsVisible() ? ConstraintSet.INVISIBLE : ConstraintSet.GONE;
-    }
-
-    private boolean areAnyExpandedBottomActionsVisible() {
-        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        for (int id : MediaViewHolder.Companion.getExpandedBottomActionIds()) {
-            if (expandedSet.getVisibility(id) == ConstraintSet.VISIBLE) {
-                return true;
-            }
-        }
-        return false;
+        // Set progress bar to INVISIBLE to keep the positions of text and buttons similar to the
+        // original positions when seekbar is enabled.
+        return ConstraintSet.INVISIBLE;
     }
 
     private void setGenericButton(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
index 647beb9..b10abb5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
@@ -48,52 +48,66 @@
     /** All commands for the sender device. */
     inner class SenderCommand : Command {
         override fun execute(pw: PrintWriter, args: List<String>) {
-            val commandName = args[1]
+            if (args.size < 2) {
+                help(pw)
+                return
+            }
+
+            val senderArgs = processArgs(args)
+
             @StatusBarManager.MediaTransferSenderState
             val displayState: Int?
             try {
-                displayState = ChipStateSender.getSenderStateIdFromName(commandName)
+                displayState = ChipStateSender.getSenderStateIdFromName(senderArgs.commandName)
             } catch (ex: IllegalArgumentException) {
-                pw.println("Invalid command name $commandName")
+                pw.println("Invalid command name ${senderArgs.commandName}")
                 return
             }
 
             @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
             val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE)
                     as StatusBarManager
-            val routeInfo = MediaRoute2Info.Builder(if (args.size >= 4) args[3] else "id", args[0])
+            val routeInfo = MediaRoute2Info.Builder(senderArgs.id, senderArgs.deviceName)
                     .addFeature("feature")
-            val useAppIcon = !(args.size >= 3 && args[2] == "useAppIcon=false")
-            if (useAppIcon) {
+            if (senderArgs.useAppIcon) {
                 routeInfo.setClientPackageName(TEST_PACKAGE_NAME)
             }
 
+            var undoExecutor: Executor? = null
+            var undoRunnable: Runnable? = null
+            if (isSucceededState(displayState) && senderArgs.showUndo) {
+                undoExecutor = mainExecutor
+                undoRunnable = Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") }
+            }
+
             statusBarManager.updateMediaTapToTransferSenderDisplay(
                 displayState,
                 routeInfo.build(),
-                getUndoExecutor(displayState),
-                getUndoCallback(displayState)
+                undoExecutor,
+                undoRunnable,
             )
         }
 
-        private fun getUndoExecutor(
-            @StatusBarManager.MediaTransferSenderState displayState: Int
-        ): Executor? {
-            return if (isSucceededState(displayState)) {
-                mainExecutor
-            } else {
-                null
-            }
-        }
+        private fun processArgs(args: List<String>): SenderArgs {
+            val senderArgs = SenderArgs(
+                deviceName = args[0],
+                commandName = args[1],
+            )
 
-        private fun getUndoCallback(
-            @StatusBarManager.MediaTransferSenderState displayState: Int
-        ): Runnable? {
-            return if (isSucceededState(displayState)) {
-                Runnable { Log.i(CLI_TAG, "Undo triggered for $displayState") }
-            } else {
-                null
+            if (args.size == 2) {
+                return senderArgs
             }
+
+            // Process any optional arguments
+            args.subList(2, args.size).forEach {
+                when {
+                    it == "useAppIcon=false" -> senderArgs.useAppIcon = false
+                    it == "showUndo=false" -> senderArgs.showUndo = false
+                    it.substring(0, 3) == "id=" -> senderArgs.id = it.substring(3)
+                }
+            }
+
+            return senderArgs
         }
 
         private fun isSucceededState(
@@ -106,14 +120,31 @@
         }
 
         override fun help(pw: PrintWriter) {
-            pw.println("Usage: adb shell cmd statusbar $SENDER_COMMAND " +
-                    "<deviceName> <chipState> useAppIcon=[true|false] <id>")
+            pw.println(
+                "Usage: adb shell cmd statusbar $SENDER_COMMAND " +
+                "<deviceName> <chipState> " +
+                "useAppIcon=[true|false] id=<id> showUndo=[true|false]"
+            )
+            pw.println("Note: useAppIcon, id, and showUndo are optional additional commands.")
         }
     }
 
+    private data class SenderArgs(
+        val deviceName: String,
+        val commandName: String,
+        var id: String = "id",
+        var useAppIcon: Boolean = true,
+        var showUndo: Boolean = true,
+    )
+
     /** All commands for the receiver device. */
     inner class ReceiverCommand : Command {
         override fun execute(pw: PrintWriter, args: List<String>) {
+            if (args.isEmpty()) {
+                help(pw)
+                return
+            }
+
             val commandName = args[0]
             @StatusBarManager.MediaTransferReceiverState
             val displayState: Int?
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index 120f7d6..b55bedd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -58,6 +58,27 @@
         )
     }
 
+    /**
+     * Logs an invalid sender state transition error in trying to update to [desiredState].
+     *
+     * @param currentState the previous state of the chip.
+     * @param desiredState the new state of the chip.
+     */
+    fun logInvalidStateTransitionError(
+        currentState: String,
+        desiredState: String
+    ) {
+        buffer.log(
+                tag,
+                LogLevel.ERROR,
+                {
+                    str1 = currentState
+                    str2 = desiredState
+                },
+                { "Cannot display state=$str2 after state=$str1; invalid transition" }
+        )
+    }
+
     /** Logs that we couldn't find information for [packageName]. */
     fun logPackageNotFound(packageName: String) {
         buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index af7317c..1f27582 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -56,7 +56,12 @@
         R.string.media_move_closer_to_start_cast,
         transferStatus = TransferStatus.NOT_STARTED,
         endItem = null,
-    ),
+    ) {
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+        }
+    },
 
     /**
      * A state representing that the two devices are close but not close enough to *end* a cast
@@ -70,7 +75,12 @@
         R.string.media_move_closer_to_end_cast,
         transferStatus = TransferStatus.NOT_STARTED,
         endItem = null,
-    ),
+    ) {
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+        }
+    },
 
     /**
      * A state representing that a transfer to the receiver device has been initiated (but not
@@ -83,7 +93,13 @@
         transferStatus = TransferStatus.IN_PROGRESS,
         endItem = SenderEndItem.Loading,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
-    ),
+    ) {
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState == TRANSFER_TO_RECEIVER_SUCCEEDED ||
+                    nextState == TRANSFER_TO_RECEIVER_FAILED
+        }
+    },
 
     /**
      * A state representing that a transfer from the receiver device and back to this device (the
@@ -96,7 +112,13 @@
         transferStatus = TransferStatus.IN_PROGRESS,
         endItem = SenderEndItem.Loading,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
-    ),
+    ) {
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState == TRANSFER_TO_THIS_DEVICE_SUCCEEDED ||
+                    nextState == TRANSFER_TO_THIS_DEVICE_FAILED
+        }
+    },
 
     /**
      * A state representing that a transfer to the receiver device has been successfully completed.
@@ -112,7 +134,13 @@
             newState =
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED
         ),
-    ),
+    ) {
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState == ALMOST_CLOSE_TO_START_CAST ||
+                    nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+        }
+    },
 
     /**
      * A state representing that a transfer back to this device has been successfully completed.
@@ -128,7 +156,13 @@
             newState =
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED
         ),
-    ),
+    ) {
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState == ALMOST_CLOSE_TO_END_CAST ||
+                    nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+        }
+    },
 
     /** A state representing that a transfer to the receiver device has failed. */
     TRANSFER_TO_RECEIVER_FAILED(
@@ -137,7 +171,13 @@
         R.string.media_transfer_failed,
         transferStatus = TransferStatus.FAILED,
         endItem = SenderEndItem.Error,
-    ),
+    ) {
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState == ALMOST_CLOSE_TO_START_CAST ||
+                    nextState == TRANSFER_TO_THIS_DEVICE_TRIGGERED
+        }
+    },
 
     /** A state representing that a transfer back to this device has failed. */
     TRANSFER_TO_THIS_DEVICE_FAILED(
@@ -146,7 +186,13 @@
         R.string.media_transfer_failed,
         transferStatus = TransferStatus.FAILED,
         endItem = SenderEndItem.Error,
-    ),
+    ) {
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState == ALMOST_CLOSE_TO_END_CAST ||
+                    nextState == TRANSFER_TO_RECEIVER_TRIGGERED
+        }
+    },
 
     /** A state representing that this device is far away from any receiver device. */
     FAR_FROM_RECEIVER(
@@ -162,6 +208,12 @@
             throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " +
                 "so its string should never be fetched")
         }
+
+        override fun isValidNextState(nextState: ChipStateSender): Boolean {
+            return nextState == FAR_FROM_RECEIVER ||
+                    nextState.transferStatus == TransferStatus.NOT_STARTED ||
+                    nextState.transferStatus == TransferStatus.IN_PROGRESS
+        }
     };
 
     /**
@@ -175,6 +227,8 @@
         return Text.Loaded(context.getString(stringResId!!, otherDeviceName))
     }
 
+    abstract fun isValidNextState(nextState: ChipStateSender): Boolean
+
     companion object {
         /**
          * Returns the sender state enum associated with the given [displayState] from
@@ -197,6 +251,31 @@
          */
         @StatusBarManager.MediaTransferSenderState
         fun getSenderStateIdFromName(name: String): Int = valueOf(name).stateInt
+
+        /**
+         * Validates the transition from a chip state to another.
+         *
+         * @param currentState is the current state of the chip.
+         * @param desiredState is the desired state of the chip.
+         * @return true if the transition from [currentState] to [desiredState] is valid, and false
+         * otherwise.
+         */
+        fun isValidStateTransition(
+                currentState: ChipStateSender?,
+                desiredState: ChipStateSender,
+        ): Boolean {
+            // Far from receiver is the default state.
+            if (currentState == null) {
+                return FAR_FROM_RECEIVER.isValidNextState(desiredState)
+            }
+
+            // No change in state is valid.
+            if (currentState == desiredState) {
+                return true
+            }
+
+            return currentState.isValidNextState(desiredState)
+        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index bb7bc6f..e34b0cb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -52,6 +52,8 @@
 ) : CoreStartable {
 
     private var displayedState: ChipStateSender? = null
+    // A map to store current chip state per id.
+    private var stateMap: MutableMap<String, ChipStateSender> = mutableMapOf()
 
     private val commandQueueCallbacks =
         object : CommandQueue.Callbacks {
@@ -87,9 +89,22 @@
             logger.logStateChangeError(displayState)
             return
         }
+
+        val currentState = stateMap[routeInfo.id]
+        if (!ChipStateSender.isValidStateTransition(currentState, chipState)) {
+            // ChipStateSender.FAR_FROM_RECEIVER is the default state when there is no state.
+            logger.logInvalidStateTransitionError(
+                currentState = currentState?.name ?: ChipStateSender.FAR_FROM_RECEIVER.name,
+                chipState.name
+            )
+            return
+        }
         uiEventLogger.logSenderStateChange(chipState)
 
+        stateMap.put(routeInfo.id, chipState)
         if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
+            // No need to store the state since it is the default state
+            stateMap.remove(routeInfo.id)
             // Return early if we're not displaying a chip anyway
             val currentDisplayedState = displayedState ?: return
 
@@ -119,7 +134,7 @@
                     context,
                     logger,
                 )
-            )
+            ) { stateMap.remove(routeInfo.id) }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 3fd1aa7..e2f55f0 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -145,7 +145,7 @@
         boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources());
         boolean largeScreenChanged = mIsTablet != isOldConfigTablet;
         // TODO(b/243765256): Disable this logging once b/243765256 is fixed.
-        Log.d(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig
+        Log.i(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig
                 + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized()
                 + " willApplyConfigToNavbars=" + willApplyConfig
                 + " navBarCount=" + mNavigationBars.size());
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 26d3902..f97385b 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -977,7 +977,7 @@
         }
 
         // TODO(b/243765256): Disable this logging once b/243765256 is fixed.
-        Log.d(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig
+        Log.i(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig
                 + " lastReportedConfig=" + mLastReportedConfig);
         mLastReportedConfig.updateFrom(newConfig);
         updateDisplaySize();
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index b964b76..6dd60d0 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -17,10 +17,12 @@
 package com.android.systemui.notetask
 
 import android.app.KeyguardManager
+import android.content.ComponentName
 import android.content.Context
+import android.content.pm.PackageManager
 import android.os.UserManager
-import android.view.KeyEvent
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.util.kotlin.getOrNull
 import com.android.wm.shell.bubbles.Bubbles
 import java.util.Optional
@@ -45,15 +47,22 @@
     @NoteTaskEnabledKey private val isEnabled: Boolean,
 ) {
 
-    fun handleSystemKey(keyCode: Int) {
+    /**
+     * Shows a note task. How the task is shown will depend on when the method is invoked.
+     *
+     * If in multi-window mode, notes will open as a full screen experience. That is particularly
+     * important for Large screen devices. These devices may support a taskbar that let users to
+     * drag and drop a shortcut into multi-window mode, and notes should comply with this behaviour.
+     *
+     * If the keyguard is locked, notes will open as a full screen experience. A locked device has
+     * no contextual information which let us use the whole screen space available.
+     *
+     * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience.
+     * That will let users open other apps in full screen, and take contextual notes.
+     */
+    fun showNoteTask(isInMultiWindowMode: Boolean = false) {
         if (!isEnabled) return
 
-        if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
-            showNoteTask()
-        }
-    }
-
-    private fun showNoteTask() {
         val bubbles = optionalBubbles.getOrNull() ?: return
         val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
         val userManager = optionalUserManager.getOrNull() ?: return
@@ -62,11 +71,35 @@
         // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
         if (!userManager.isUserUnlocked) return
 
-        if (keyguardManager.isKeyguardLocked) {
+        if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
             context.startActivity(intent)
         } else {
             // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
             bubbles.showAppBubble(intent)
         }
     }
+
+    /**
+     * Set `android:enabled` property in the `AndroidManifest` associated with the Shortcut
+     * component to [value].
+     *
+     * If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the
+     * Widget Picker to all users.
+     */
+    fun setNoteTaskShortcutEnabled(value: Boolean) {
+        val componentName = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
+
+        val enabledState =
+            if (value) {
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+            } else {
+                PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+            }
+
+        context.packageManager.setComponentEnabledSetting(
+            componentName,
+            enabledState,
+            PackageManager.DONT_KILL_APP,
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index 0a5b600..d14b7a7 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,9 +16,10 @@
 
 package com.android.systemui.notetask
 
+import android.view.KeyEvent
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.statusbar.CommandQueue
 import com.android.wm.shell.bubbles.Bubbles
-import dagger.Lazy
 import java.util.Optional
 import javax.inject.Inject
 
@@ -27,15 +28,18 @@
 @Inject
 constructor(
     private val optionalBubbles: Optional<Bubbles>,
-    private val lazyNoteTaskController: Lazy<NoteTaskController>,
+    private val noteTaskController: NoteTaskController,
     private val commandQueue: CommandQueue,
     @NoteTaskEnabledKey private val isEnabled: Boolean,
 ) {
 
-    private val callbacks =
+    @VisibleForTesting
+    val callbacks =
         object : CommandQueue.Callbacks {
             override fun handleSystemKey(keyCode: Int) {
-                lazyNoteTaskController.get().handleSystemKey(keyCode)
+                if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
+                    noteTaskController.showNoteTask()
+                }
             }
         }
 
@@ -43,5 +47,6 @@
         if (isEnabled && optionalBubbles.isPresent) {
             commandQueue.addCallback(callbacks)
         }
+        noteTaskController.setNoteTaskShortcutEnabled(isEnabled)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index 035396a..8bdf319 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -16,32 +16,47 @@
 
 package com.android.systemui.notetask
 
+import android.app.Activity
 import android.app.KeyguardManager
 import android.content.Context
 import android.os.UserManager
 import androidx.core.content.getSystemService
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
+import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
+import dagger.Binds
 import dagger.Module
 import dagger.Provides
-import java.util.*
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.util.Optional
 
 /** Compose all dependencies required by Note Task feature. */
 @Module
-internal class NoteTaskModule {
+internal interface NoteTaskModule {
 
-    @[Provides NoteTaskEnabledKey]
-    fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
-        return featureFlags.isEnabled(Flags.NOTE_TASKS)
-    }
+    @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
+    fun bindNoteTaskLauncherActivity(activity: LaunchNoteTaskActivity): Activity?
 
-    @Provides
-    fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> {
-        return Optional.ofNullable(context.getSystemService())
-    }
+    @[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)]
+    fun bindNoteTaskShortcutActivity(activity: CreateNoteTaskShortcutActivity): Activity?
 
-    @Provides
-    fun provideOptionalUserManager(context: Context): Optional<UserManager> {
-        return Optional.ofNullable(context.getSystemService())
+    companion object {
+
+        @[Provides NoteTaskEnabledKey]
+        fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
+            return featureFlags.isEnabled(Flags.NOTE_TASKS)
+        }
+
+        @Provides
+        fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> {
+            return Optional.ofNullable(context.getSystemService())
+        }
+
+        @Provides
+        fun provideOptionalUserManager(context: Context): Optional<UserManager> {
+            return Optional.ofNullable(context.getSystemService())
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
new file mode 100644
index 0000000..f6a623e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.notetask.shortcut
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.annotation.DrawableRes
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+import com.android.systemui.R
+import javax.inject.Inject
+
+/**
+ * Activity responsible for create a shortcut for notes action. If the shortcut is enabled, a new
+ * shortcut will appear in the widget picker. If the shortcut is selected, the Activity here will be
+ * launched, creating a new shortcut for [CreateNoteTaskShortcutActivity], and will finish.
+ *
+ * @see <a
+ * href="https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts#custom-pinned">Creating
+ * a custom shortcut activity</a>
+ */
+internal class CreateNoteTaskShortcutActivity @Inject constructor() : ComponentActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val intent =
+            createShortcutIntent(
+                id = SHORTCUT_ID,
+                shortLabel = getString(R.string.note_task_button_label),
+                intent = LaunchNoteTaskActivity.newIntent(context = this),
+                iconResource = R.drawable.ic_note_task_button,
+            )
+        setResult(Activity.RESULT_OK, intent)
+
+        finish()
+    }
+
+    private fun createShortcutIntent(
+        id: String,
+        shortLabel: String,
+        intent: Intent,
+        @DrawableRes iconResource: Int,
+    ): Intent {
+        val shortcutInfo =
+            ShortcutInfoCompat.Builder(this, id)
+                .setIntent(intent)
+                .setShortLabel(shortLabel)
+                .setLongLived(true)
+                .setIcon(IconCompat.createWithResource(this, iconResource))
+                .build()
+
+        return ShortcutManagerCompat.createShortcutResultIntent(
+            this,
+            shortcutInfo,
+        )
+    }
+
+    private companion object {
+        private const val SHORTCUT_ID = "note-task-shortcut-id"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
new file mode 100644
index 0000000..47fe676
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.notetask.shortcut
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskIntentResolver
+import javax.inject.Inject
+
+/** Activity responsible for launching the note experience, and finish. */
+internal class LaunchNoteTaskActivity
+@Inject
+constructor(
+    private val noteTaskController: NoteTaskController,
+) : ComponentActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        noteTaskController.showNoteTask(isInMultiWindowMode)
+
+        finish()
+    }
+
+    companion object {
+
+        /** Creates a new [Intent] set to start [LaunchNoteTaskActivity]. */
+        fun newIntent(context: Context): Intent {
+            return Intent(context, LaunchNoteTaskActivity::class.java).apply {
+                // Intent's action must be set in shortcuts, or an exception will be thrown.
+                // TODO(b/254606432): Use Intent.ACTION_NOTES instead.
+                action = NoteTaskIntentResolver.NOTES_ACTION
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
index 67bc769..5dbf0f8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
@@ -247,7 +247,7 @@
 
         Icon icon;
         ContentDescription contentDescription = null;
-        if (isParentalControlsEnabled) {
+        if (isParentalControlsEnabled && securityModel.getDeviceAdminIcon() != null) {
             icon = new Icon.Loaded(securityModel.getDeviceAdminIcon(), contentDescription);
         } else if (vpnName != null || vpnNameWorkProfile != null) {
             if (securityModel.isVpnBranded()) {
@@ -476,7 +476,7 @@
     @VisibleForTesting
     View createDialogView(Context quickSettingsContext) {
         if (mSecurityController.isParentalControlsEnabled()) {
-            return createParentalControlsDialogView();
+            return createParentalControlsDialogView(quickSettingsContext);
         }
         return createOrganizationDialogView(quickSettingsContext);
     }
@@ -579,8 +579,8 @@
         return dialogView;
     }
 
-    private View createParentalControlsDialogView() {
-        View dialogView = LayoutInflater.from(mContext)
+    private View createParentalControlsDialogView(Context quickSettingsContext) {
+        View dialogView = LayoutInflater.from(quickSettingsContext)
                 .inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false);
 
         DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 6240c10..cad296b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -608,7 +608,7 @@
 
         if (TextUtils.isEmpty(tileList)) {
             tileList = res.getString(R.string.quick_settings_tiles);
-            if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList);
+            if (DEBUG) Log.d(TAG, "Loaded tile specs from default config: " + tileList);
         } else {
             if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 350d8b0..9542a02 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -188,8 +188,6 @@
         int mWifiSignalIconId;
         @Nullable
         String mSsid;
-        boolean mActivityIn;
-        boolean mActivityOut;
         @Nullable
         String mWifiSignalContentDescription;
         boolean mIsTransient;
@@ -207,8 +205,6 @@
                     .append(",mConnected=").append(mConnected)
                     .append(",mWifiSignalIconId=").append(mWifiSignalIconId)
                     .append(",mSsid=").append(mSsid)
-                    .append(",mActivityIn=").append(mActivityIn)
-                    .append(",mActivityOut=").append(mActivityOut)
                     .append(",mWifiSignalContentDescription=").append(mWifiSignalContentDescription)
                     .append(",mIsTransient=").append(mIsTransient)
                     .append(",mNoDefaultNetwork=").append(mNoDefaultNetwork)
@@ -226,8 +222,6 @@
         CharSequence mDataContentDescription;
         int mMobileSignalIconId;
         int mQsTypeIcon;
-        boolean mActivityIn;
-        boolean mActivityOut;
         boolean mNoSim;
         boolean mRoaming;
         boolean mMultipleSubs;
@@ -243,8 +237,6 @@
                 .append(",mDataContentDescription=").append(mDataContentDescription)
                 .append(",mMobileSignalIconId=").append(mMobileSignalIconId)
                 .append(",mQsTypeIcon=").append(mQsTypeIcon)
-                .append(",mActivityIn=").append(mActivityIn)
-                .append(",mActivityOut=").append(mActivityOut)
                 .append(",mNoSim=").append(mNoSim)
                 .append(",mRoaming=").append(mRoaming)
                 .append(",mMultipleSubs=").append(mMultipleSubs)
@@ -275,8 +267,6 @@
             mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
             mWifiInfo.mEnabled = indicators.enabled;
             mWifiInfo.mSsid = indicators.description;
-            mWifiInfo.mActivityIn = indicators.activityIn;
-            mWifiInfo.mActivityOut = indicators.activityOut;
             mWifiInfo.mIsTransient = indicators.isTransient;
             mWifiInfo.mStatusLabel = indicators.statusLabel;
             refreshState(mWifiInfo);
@@ -297,8 +287,6 @@
                     ? indicators.typeContentDescriptionHtml : null;
             mCellularInfo.mMobileSignalIconId = indicators.qsIcon.icon;
             mCellularInfo.mQsTypeIcon = indicators.qsType;
-            mCellularInfo.mActivityIn = indicators.activityIn;
-            mCellularInfo.mActivityOut = indicators.activityOut;
             mCellularInfo.mRoaming = indicators.roaming;
             mCellularInfo.mMultipleSubs = mController.getNumberSubscriptions() > 1;
             refreshState(mCellularInfo);
@@ -428,8 +416,6 @@
         state.state = Tile.STATE_ACTIVE;
         state.dualTarget = true;
         state.value = cb.mEnabled;
-        state.activityIn = cb.mEnabled && cb.mActivityIn;
-        state.activityOut = cb.mEnabled && cb.mActivityOut;
         final StringBuffer minimalContentDescription = new StringBuffer();
         final StringBuffer minimalStateDescription = new StringBuffer();
         final Resources r = mContext.getResources();
@@ -503,8 +489,6 @@
         boolean mobileDataEnabled = mDataController.isMobileDataSupported()
                 && mDataController.isMobileDataEnabled();
         state.value = mobileDataEnabled;
-        state.activityIn = mobileDataEnabled && cb.mActivityIn;
-        state.activityOut = mobileDataEnabled && cb.mActivityOut;
         state.expandedAccessibilityClassName = Switch.class.getName();
 
         if (cb.mAirplaneModeEnabled && cb.mQsTypeIcon != TelephonyIcons.ICON_CWF) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index 7130294..a6c7781 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -27,6 +27,7 @@
 import android.view.View;
 import android.widget.Switch;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
@@ -91,11 +92,13 @@
     }
 
     @Override
+    @MainThread
     public void onManagedProfileChanged() {
         refreshState(mProfileController.isWorkModeEnabled());
     }
 
     @Override
+    @MainThread
     public void onManagedProfileRemoved() {
         mHost.removeTile(getTileSpec());
         mHost.unmarkTileAsAutoAdded(getTileSpec());
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 547b496..00d129a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -565,13 +565,25 @@
         statusBarWinController.registerCallback(mStatusBarWindowCallback);
         mScreenshotHelper = new ScreenshotHelper(context);
 
-        // Listen for tracing state changes
         commandQueue.addCallback(new CommandQueue.Callbacks() {
+
+            // Listen for tracing state changes
             @Override
             public void onTracingStateChanged(boolean enabled) {
                 mSysUiState.setFlag(SYSUI_STATE_TRACING_ENABLED, enabled)
                         .commitUpdate(mContext.getDisplayId());
             }
+
+            @Override
+            public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+                if (mOverviewProxy != null) {
+                    try {
+                        mOverviewProxy.enterStageSplitFromRunningApp(leftOrTop);
+                    } catch (RemoteException e) {
+                        Log.w(TAG_OPS, "Unable to enter stage split from the current running app");
+                    }
+                }
+            }
         });
         mCommandQueue = commandQueue;
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index 5e47d6d..b511b54 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -46,7 +46,6 @@
 import com.android.systemui.qs.carrier.QSCarrierGroupController
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.HEADER_TRANSITION_ID
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
@@ -73,11 +72,9 @@
  * expansion of the headers in small screen portrait.
  *
  * [header] will be a [MotionLayout] if [Flags.COMBINED_QS_HEADERS] is enabled. In this case, the
- * [MotionLayout] has 2 transitions:
+ * [MotionLayout] has one transitions:
  * * [HEADER_TRANSITION_ID]: [QQS_HEADER_CONSTRAINT] <-> [QS_HEADER_CONSTRAINT] for portrait
  *   handheld device configuration.
- * * [LARGE_SCREEN_HEADER_TRANSITION_ID]: [LARGE_SCREEN_HEADER_CONSTRAINT] (to itself) for all
- *   other configurations
  */
 @CentralSurfacesScope
 class LargeScreenShadeHeaderController @Inject constructor(
@@ -104,8 +101,6 @@
         @VisibleForTesting
         internal val HEADER_TRANSITION_ID = R.id.header_transition
         @VisibleForTesting
-        internal val LARGE_SCREEN_HEADER_TRANSITION_ID = R.id.large_screen_header_transition
-        @VisibleForTesting
         internal val QQS_HEADER_CONSTRAINT = R.id.qqs_header_constraint
         @VisibleForTesting
         internal val QS_HEADER_CONSTRAINT = R.id.qs_header_constraint
@@ -120,10 +115,6 @@
         }
     }
 
-    init {
-        loadConstraints()
-    }
-
     private val combinedHeaders = featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)
 
     private lateinit var iconManager: StatusBarIconController.TintedIconManager
@@ -345,11 +336,11 @@
         if (header is MotionLayout) {
             // Use resources.getXml instead of passing the resource id due to bug b/205018300
             header.getConstraintSet(QQS_HEADER_CONSTRAINT)
-                .load(context, resources.getXml(R.xml.qqs_header))
+                    .load(context, resources.getXml(R.xml.qqs_header))
             header.getConstraintSet(QS_HEADER_CONSTRAINT)
-                .load(context, resources.getXml(R.xml.qs_header))
+                    .load(context, resources.getXml(R.xml.qs_header))
             header.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT)
-                .load(context, resources.getXml(R.xml.large_screen_shade_header))
+                    .load(context, resources.getXml(R.xml.large_screen_shade_header))
         }
     }
 
@@ -438,7 +429,6 @@
         }
         header as MotionLayout
         if (largeScreenActive) {
-            header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
             header.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT).applyTo(header)
         } else {
             header.setTransition(HEADER_TRANSITION_ID)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 6f71b72..507dec6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -465,7 +465,11 @@
     private boolean mQsTouchAboveFalsingThreshold;
     private int mQsFalsingThreshold;
 
-    /** Indicates drag starting height when swiping down or up on heads-up notifications */
+    /**
+     * Indicates drag starting height when swiping down or up on heads-up notifications.
+     * This usually serves as a threshold from when shade expansion should really start. Otherwise
+     * this value would be height of shade and it will be immediately expanded to some extent.
+     */
     private int mHeadsUpStartHeight;
     private HeadsUpTouchHelper mHeadsUpTouchHelper;
     private boolean mListenForHeadsUp;
@@ -915,6 +919,7 @@
         mQsFrameTranslateController = qsFrameTranslateController;
         updateUserSwitcherFlags();
         mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
+        mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
         onFinishInflate();
         keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
                 new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
@@ -932,7 +937,6 @@
                         unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay);
                     }
                 });
-        mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
         dumpManager.registerDumpable(this);
     }
 
@@ -1108,6 +1112,7 @@
                 mKeyguardStatusViewComponentFactory.build(keyguardStatusView);
         mKeyguardStatusViewController = statusViewComponent.getKeyguardStatusViewController();
         mKeyguardStatusViewController.init();
+        updateClockAppearance();
 
         if (mKeyguardUserSwitcherController != null) {
             // Try to close the switcher so that callbacks are triggered if necessary.
@@ -3411,9 +3416,12 @@
                 && mQsExpansionAnimator == null && !mQsExpansionFromOverscroll;
         boolean goingBetweenClosedShadeAndExpandedQs =
                 mQsExpandImmediate || collapsingShadeFromExpandedQs;
-        // we don't want to update QS expansion when HUN is visible because then the whole shade is
-        // initially hidden, even though it has non-zero height
-        if (goingBetweenClosedShadeAndExpandedQs && !mHeadsUpManager.isTrackingHeadsUp()) {
+        // in split shade we react when HUN is visible only if shade height is over HUN start
+        // height - which means user is swiping down. Otherwise shade QS will either not show at all
+        // with HUN movement or it will blink when touching HUN initially
+        boolean qsShouldExpandWithHeadsUp = !mSplitShadeEnabled
+                || (!mHeadsUpManager.isTrackingHeadsUp() || expandedHeight > mHeadsUpStartHeight);
+        if (goingBetweenClosedShadeAndExpandedQs && qsShouldExpandWithHeadsUp) {
             float qsExpansionFraction;
             if (mSplitShadeEnabled) {
                 qsExpansionFraction = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 1dd3a96..590a04a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -164,6 +164,8 @@
     private static final int MSG_UNREGISTER_NEARBY_MEDIA_DEVICE_PROVIDER = 67 << MSG_SHIFT;
     private static final int MSG_TILE_SERVICE_REQUEST_LISTENING_STATE = 68 << MSG_SHIFT;
     private static final int MSG_SHOW_REAR_DISPLAY_DIALOG = 69 << MSG_SHIFT;
+    private static final int MSG_GO_TO_FULLSCREEN_FROM_SPLIT = 70 << MSG_SHIFT;
+    private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT;
 
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -478,6 +480,16 @@
          * @see IStatusBar#showRearDisplayDialog
          */
         default void showRearDisplayDialog(int currentBaseState) {}
+
+        /**
+         * @see IStatusBar#goToFullscreenFromSplit
+         */
+        default void goToFullscreenFromSplit() {}
+
+        /**
+         * @see IStatusBar#enterStageSplitFromRunningApp
+         */
+        default void enterStageSplitFromRunningApp(boolean leftOrTop) {}
     }
 
     public CommandQueue(Context context) {
@@ -1239,6 +1251,14 @@
     }
 
     @Override
+    public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP,
+                    leftOrTop).sendToTarget();
+        }
+    }
+
+    @Override
     public void requestAddTile(
             @NonNull ComponentName componentName,
             @NonNull CharSequence appName,
@@ -1299,6 +1319,11 @@
                 .sendToTarget();
     }
 
+    @Override
+    public void goToFullscreenFromSplit() {
+        mHandler.obtainMessage(MSG_GO_TO_FULLSCREEN_FROM_SPLIT).sendToTarget();
+    }
+
     private final class H extends Handler {
         private H(Looper l) {
             super(l);
@@ -1738,6 +1763,17 @@
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).showRearDisplayDialog((Integer) msg.obj);
                     }
+                    break;
+                case MSG_GO_TO_FULLSCREEN_FROM_SPLIT:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).goToFullscreenFromSplit();
+                    }
+                    break;
+                case MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).enterStageSplitFromRunningApp((Boolean) msg.obj);
+                    }
+                    break;
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index d7eddf5..56c34a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -39,6 +39,7 @@
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.statusbar.notification.LegacySourceType;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -66,6 +67,8 @@
     // the next icon has translated out of the way, to avoid overlapping.
     private static final Interpolator ICON_ALPHA_INTERPOLATOR =
             new PathInterpolator(0.6f, 0f, 0.6f, 0f);
+    private static final SourceType BASE_VALUE = SourceType.from("BaseValue");
+    private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll");
 
     private NotificationIconContainer mShelfIcons;
     private int[] mTmp = new int[2];
@@ -112,19 +115,24 @@
         setClipChildren(false);
         setClipToPadding(false);
         mShelfIcons.setIsStaticLayout(false);
-        requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue);
-        requestTopRoundness(1f, false, SourceType.DefaultValue);
+        requestRoundness(/* top = */ 1f, /* bottom = */ 1f, BASE_VALUE, /* animate = */ false);
 
-        // Setting this to first in section to get the clipping to the top roundness correct. This
-        // value determines the way we are clipping to the top roundness of the overall shade
-        setFirstInSection(true);
+        if (!mUseRoundnessSourceTypes) {
+            // Setting this to first in section to get the clipping to the top roundness correct.
+            // This value determines the way we are clipping to the top roundness of the overall
+            // shade
+            setFirstInSection(true);
+        }
         updateResources();
     }
 
     public void bind(AmbientState ambientState,
-            NotificationStackScrollLayoutController hostLayoutController) {
+                     NotificationStackScrollLayoutController hostLayoutController) {
         mAmbientState = ambientState;
         mHostLayoutController = hostLayoutController;
+        hostLayoutController.setOnNotificationRemovedListener((child, isTransferInProgress) -> {
+            child.requestRoundnessReset(SHELF_SCROLL);
+        });
     }
 
     private void updateResources() {
@@ -185,9 +193,11 @@
                 + " indexOfFirstViewInShelf=" + mIndexOfFirstViewInShelf + ')';
     }
 
-    /** Update the state of the shelf. */
+    /**
+     * Update the state of the shelf.
+     */
     public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState) {
+                            AmbientState ambientState) {
         ExpandableView lastView = ambientState.getLastVisibleBackgroundChild();
         ShelfState viewState = (ShelfState) getViewState();
         if (mShowNotificationShelf && lastView != null) {
@@ -246,7 +256,7 @@
 
     /**
      * @param fractionToShade Fraction of lockscreen to shade transition
-     * @param shortestWidth Shortest width to use for lockscreen shelf
+     * @param shortestWidth   Shortest width to use for lockscreen shelf
      */
     @VisibleForTesting
     public void updateActualWidth(float fractionToShade, float shortestWidth) {
@@ -281,9 +291,9 @@
 
     /**
      * @param localX Click x from left of screen
-     * @param slop Margin of error within which we count x for valid click
-     * @param left Left of shelf, from left of screen
-     * @param right Right of shelf, from left of screen
+     * @param slop   Margin of error within which we count x for valid click
+     * @param left   Left of shelf, from left of screen
+     * @param right  Right of shelf, from left of screen
      * @return Whether click x was in view
      */
     @VisibleForTesting
@@ -293,8 +303,8 @@
 
     /**
      * @param localY Click y from top of shelf
-     * @param slop Margin of error within which we count y for valid click
-     * @param top Top of shelf
+     * @param slop   Margin of error within which we count y for valid click
+     * @param top    Top of shelf
      * @param bottom Height of shelf
      * @return Whether click y was in view
      */
@@ -306,7 +316,7 @@
     /**
      * @param localX Click x
      * @param localY Click y
-     * @param slop Margin of error for valid click
+     * @param slop   Margin of error for valid click
      * @return Whether this click was on the visible (non-clipped) part of the shelf
      */
     @Override
@@ -478,13 +488,15 @@
         }
     }
 
-    private void updateCornerRoundnessOnScroll(ActivatableNotificationView anv, float viewStart,
+    private void updateCornerRoundnessOnScroll(
+            ActivatableNotificationView anv,
+            float viewStart,
             float shelfStart) {
 
         final boolean isUnlockedHeadsUp = !mAmbientState.isOnKeyguard()
                 && !mAmbientState.isShadeExpanded()
                 && anv instanceof ExpandableNotificationRow
-                && ((ExpandableNotificationRow) anv).isHeadsUp();
+                && anv.isHeadsUp();
 
         final boolean isHunGoingToShade = mAmbientState.isShadeExpanded()
                 && anv == mAmbientState.getTrackedHeadsUpRow();
@@ -506,41 +518,40 @@
                 * mAmbientState.getExpansionFraction();
         final float cornerAnimationTop = shelfStart - cornerAnimationDistance;
 
-        if (viewEnd >= cornerAnimationTop) {
-            // Round bottom corners within animation bounds
-            final float changeFraction = MathUtils.saturate(
-                    (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
-            anv.requestBottomRoundness(
-                    /* value = */ anv.isLastInSection() ? 1f : changeFraction,
-                    /* animate = */ false,
-                    SourceType.OnScroll);
-
-        } else if (viewEnd < cornerAnimationTop) {
-            // Fast scroll skips frames and leaves corners with unfinished rounding.
-            // Reset top and bottom corners outside of animation bounds.
-            anv.requestBottomRoundness(
-                    /* value = */ anv.isLastInSection() ? 1f : 0f,
-                    /* animate = */ false,
-                    SourceType.OnScroll);
+        final SourceType sourceType;
+        if (mUseRoundnessSourceTypes) {
+            sourceType = SHELF_SCROLL;
+        } else {
+            sourceType = LegacySourceType.OnScroll;
         }
 
-        if (viewStart >= cornerAnimationTop) {
+        final float topValue;
+        if (!mUseRoundnessSourceTypes && anv.isFirstInSection()) {
+            topValue = 1f;
+        } else if (viewStart >= cornerAnimationTop) {
             // Round top corners within animation bounds
-            final float changeFraction = MathUtils.saturate(
+            topValue = MathUtils.saturate(
                     (viewStart - cornerAnimationTop) / cornerAnimationDistance);
-            anv.requestTopRoundness(
-                    /* value = */ anv.isFirstInSection() ? 1f : changeFraction,
-                    /* animate = */ false,
-                    SourceType.OnScroll);
-
-        } else if (viewStart < cornerAnimationTop) {
+        } else {
             // Fast scroll skips frames and leaves corners with unfinished rounding.
             // Reset top and bottom corners outside of animation bounds.
-            anv.requestTopRoundness(
-                    /* value = */ anv.isFirstInSection() ? 1f : 0f,
-                    /* animate = */ false,
-                    SourceType.OnScroll);
+            topValue = 0f;
         }
+        anv.requestTopRoundness(topValue, sourceType, /* animate = */ false);
+
+        final float bottomValue;
+        if (!mUseRoundnessSourceTypes && anv.isLastInSection()) {
+            bottomValue = 1f;
+        } else if (viewEnd >= cornerAnimationTop) {
+            // Round bottom corners within animation bounds
+            bottomValue = MathUtils.saturate(
+                    (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
+        } else {
+            // Fast scroll skips frames and leaves corners with unfinished rounding.
+            // Reset top and bottom corners outside of animation bounds.
+            bottomValue = 0f;
+        }
+        anv.requestBottomRoundness(bottomValue, sourceType, /* animate = */ false);
     }
 
     /**
@@ -626,10 +637,11 @@
 
     /**
      * Update the clipping of this view.
+     *
      * @return the amount that our own top should be clipped
      */
     private int updateNotificationClipHeight(ExpandableView view,
-            float notificationClipEnd, int childIndex) {
+                                             float notificationClipEnd, int childIndex) {
         float viewEnd = view.getTranslationY() + view.getActualHeight();
         boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway())
                 && !mAmbientState.isDozingAndNotPulsing(view);
@@ -657,7 +669,7 @@
 
     @Override
     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
-            int outlineTranslation) {
+                                       int outlineTranslation) {
         if (!mHasItemsInStableShelf) {
             shadowIntensity = 0.0f;
         }
@@ -665,18 +677,24 @@
     }
 
     /**
-     * @param i Index of the view in the host layout.
-     * @param view The current ExpandableView.
-     * @param scrollingFast Whether we are scrolling fast.
+     * @param i                 Index of the view in the host layout.
+     * @param view              The current ExpandableView.
+     * @param scrollingFast     Whether we are scrolling fast.
      * @param expandingAnimated Whether we are expanding a notification.
-     * @param isLastChild Whether this is the last view.
-     * @param shelfClipStart The point at which notifications start getting clipped by the shelf.
+     * @param isLastChild       Whether this is the last view.
+     * @param shelfClipStart    The point at which notifications start getting clipped by the shelf.
      * @return The amount how much this notification is in the shelf.
-     *         0f is not in shelf. 1f is completely in shelf.
+     * 0f is not in shelf. 1f is completely in shelf.
      */
     @VisibleForTesting
-    public float getAmountInShelf(int i, ExpandableView view, boolean scrollingFast,
-            boolean expandingAnimated, boolean isLastChild, float shelfClipStart) {
+    public float getAmountInShelf(
+            int i,
+            ExpandableView view,
+            boolean scrollingFast,
+            boolean expandingAnimated,
+            boolean isLastChild,
+            float shelfClipStart
+    ) {
 
         // Let's calculate how much the view is in the shelf
         float viewStart = view.getTranslationY();
@@ -755,8 +773,13 @@
         return start;
     }
 
-    private void updateIconPositioning(ExpandableView view, float iconTransitionAmount,
-            boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
+    private void updateIconPositioning(
+            ExpandableView view,
+            float iconTransitionAmount,
+            boolean scrollingFast,
+            boolean expandingAnimated,
+            boolean isLastChild
+    ) {
         StatusBarIconView icon = view.getShelfIcon();
         NotificationIconContainer.IconState iconState = getIconState(icon);
         if (iconState == null) {
@@ -817,7 +840,7 @@
                 || row.showingPulsing()
                 || row.getTranslationZ() > mAmbientState.getBaseZHeight();
 
-        iconState.iconAppearAmount = iconState.hidden? 0f : transitionAmount;
+        iconState.iconAppearAmount = iconState.hidden ? 0f : transitionAmount;
 
         // Fade in icons at shelf start
         // This is important for conversation icons, which are badged and need x reset
@@ -847,7 +870,7 @@
     }
 
     private float getFullyClosedTranslation() {
-        return - (getIntrinsicHeight() - mStatusBarHeight) / 2;
+        return -(getIntrinsicHeight() - mStatusBarHeight) / 2;
     }
 
     @Override
@@ -904,7 +927,7 @@
 
     /**
      * @return whether the shelf has any icons in it when a potential animation has finished, i.e
-     *         if the current state would be applied right now
+     * if the current state would be applied right now
      */
     public boolean hasItemsInStableShelf() {
         return mHasItemsInStableShelf;
@@ -962,7 +985,7 @@
 
     @Override
     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
-            int oldTop, int oldRight, int oldBottom) {
+                               int oldTop, int oldRight, int oldBottom) {
         updateRelativeOffset();
     }
 
@@ -981,12 +1004,11 @@
 
     /**
      * This method resets the OnScroll roundness of a view to 0f
-     *
+     * <p>
      * Note: This should be the only class that handles roundness {@code SourceType.OnScroll}
      */
-    public static void resetOnScrollRoundness(ExpandableView expandableView) {
-        expandableView.requestTopRoundness(0f, false, SourceType.OnScroll);
-        expandableView.requestBottomRoundness(0f, false, SourceType.OnScroll);
+    public static void resetLegacyOnScrollRoundness(ExpandableView expandableView) {
+        expandableView.requestRoundnessReset(LegacySourceType.OnScroll);
     }
 
     public class ShelfState extends ExpandableViewState {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java
index 3b1fa17..bb84c75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java
@@ -18,6 +18,8 @@
 
 import android.view.View;
 
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope;
@@ -42,14 +44,17 @@
     private AmbientState mAmbientState;
 
     @Inject
-    public NotificationShelfController(NotificationShelf notificationShelf,
+    public NotificationShelfController(
+            NotificationShelf notificationShelf,
             ActivatableNotificationViewController activatableNotificationViewController,
             KeyguardBypassController keyguardBypassController,
-            SysuiStatusBarStateController statusBarStateController) {
+            SysuiStatusBarStateController statusBarStateController,
+            FeatureFlags featureFlags) {
         mView = notificationShelf;
         mActivatableNotificationViewController = activatableNotificationViewController;
         mKeyguardBypassController = keyguardBypassController;
         mStatusBarStateController = statusBarStateController;
+        mView.useRoundnessSourceTypes(featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES));
         mOnAttachStateChangeListener = new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -88,7 +93,7 @@
 
     public @View.Visibility int getVisibility() {
         return mView.getVisibility();
-    };
+    }
 
     public void setCollapsedIcons(NotificationIconContainer notificationIcons) {
         mView.setCollapsedIcons(notificationIcons);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 99ff06a..336356e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -68,7 +68,7 @@
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.LongRunning;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
@@ -78,6 +78,7 @@
 import com.android.systemui.plugins.log.LogLevel;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DataSaverController;
 import com.android.systemui.statusbar.policy.DataSaverControllerImpl;
@@ -192,6 +193,7 @@
     private final Executor mBgExecutor;
     // Handler that all callbacks are made on.
     private final CallbackHandler mCallbackHandler;
+    private final StatusBarPipelineFlags mStatusBarPipelineFlags;
 
     private int mEmergencySource;
     private boolean mIsEmergency;
@@ -223,12 +225,15 @@
 
     /**
      * Construct this controller object and register for updates.
+     *
+     * {@code @LongRunning} looper and bgExecutor instead {@code @Background} ones are used to
+     * address the b/246456655. This can be reverted after b/240663726 is fixed.
      */
     @Inject
     public NetworkControllerImpl(
             Context context,
-            @Background Looper bgLooper,
-            @Background Executor bgExecutor,
+            @LongRunning Looper bgLooper,
+            @LongRunning Executor bgExecutor,
             SubscriptionManager subscriptionManager,
             CallbackHandler callbackHandler,
             DeviceProvisionedController deviceProvisionedController,
@@ -239,6 +244,7 @@
             TelephonyListenerManager telephonyListenerManager,
             @Nullable WifiManager wifiManager,
             AccessPointControllerImpl accessPointController,
+            StatusBarPipelineFlags statusBarPipelineFlags,
             DemoModeController demoModeController,
             CarrierConfigTracker carrierConfigTracker,
             WifiStatusTrackerFactory trackerFactory,
@@ -257,6 +263,7 @@
                 bgExecutor,
                 callbackHandler,
                 accessPointController,
+                statusBarPipelineFlags,
                 new DataUsageController(context),
                 new SubscriptionDefaults(),
                 deviceProvisionedController,
@@ -284,6 +291,7 @@
             Executor bgExecutor,
             CallbackHandler callbackHandler,
             AccessPointControllerImpl accessPointController,
+            StatusBarPipelineFlags statusBarPipelineFlags,
             DataUsageController dataUsageController,
             SubscriptionDefaults defaultsHandler,
             DeviceProvisionedController deviceProvisionedController,
@@ -305,6 +313,7 @@
         mBgLooper = bgLooper;
         mBgExecutor = bgExecutor;
         mCallbackHandler = callbackHandler;
+        mStatusBarPipelineFlags = statusBarPipelineFlags;
         mDataSaverController = new DataSaverControllerImpl(context);
         mBroadcastDispatcher = broadcastDispatcher;
         mMobileFactory = mobileFactory;
@@ -1330,7 +1339,7 @@
             mWifiSignalController.notifyListeners();
         }
         String sims = args.getString("sims");
-        if (sims != null) {
+        if (sims != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
             int num = MathUtils.constrain(Integer.parseInt(sims), 1, 8);
             List<SubscriptionInfo> subs = new ArrayList<>();
             if (num != mMobileSignalControllers.size()) {
@@ -1353,7 +1362,7 @@
             mCallbackHandler.setNoSims(mHasNoSubs, mSimDetected);
         }
         String mobile = args.getString("mobile");
-        if (mobile != null) {
+        if (mobile != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
             boolean show = mobile.equals("show");
             String datatype = args.getString("datatype");
             String slotString = args.getString("slot");
@@ -1438,7 +1447,7 @@
             controller.notifyListeners();
         }
         String carrierNetworkChange = args.getString("carriernetworkchange");
-        if (carrierNetworkChange != null) {
+        if (carrierNetworkChange != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
             boolean show = carrierNetworkChange.equals("show");
             for (int i = 0; i < mMobileSignalControllers.size(); i++) {
                 MobileSignalController controller = mMobileSignalControllers.valueAt(i);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index ed7f648..0eb0000 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -74,8 +74,8 @@
     @JvmDefault
     fun requestTopRoundness(
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        animate: Boolean,
         sourceType: SourceType,
+        animate: Boolean,
     ): Boolean {
         val roundnessMap = roundableState.topRoundnessMap
         val lastValue = roundnessMap.values.maxOrNull() ?: 0f
@@ -105,6 +105,30 @@
     }
 
     /**
+     * Request the top roundness [value] for a specific [sourceType]. Animate the roundness if the
+     * view is shown.
+     *
+     * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
+     * origins require different roundness, for the same property, the maximum value will always be
+     * chosen.
+     *
+     * @param value a value between 0f and 1f.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestTopRoundness(
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        sourceType: SourceType,
+    ): Boolean {
+        return requestTopRoundness(
+            value = value,
+            sourceType = sourceType,
+            animate = roundableState.targetView.isShown
+        )
+    }
+
+    /**
      * Request the bottom roundness [value] for a specific [sourceType].
      *
      * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
@@ -119,8 +143,8 @@
     @JvmDefault
     fun requestBottomRoundness(
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        animate: Boolean,
         sourceType: SourceType,
+        animate: Boolean,
     ): Boolean {
         val roundnessMap = roundableState.bottomRoundnessMap
         val lastValue = roundnessMap.values.maxOrNull() ?: 0f
@@ -149,9 +173,101 @@
         return false
     }
 
+    /**
+     * Request the bottom roundness [value] for a specific [sourceType]. Animate the roundness if
+     * the view is shown.
+     *
+     * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
+     * origins require different roundness, for the same property, the maximum value will always be
+     * chosen.
+     *
+     * @param value value between 0f and 1f.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestBottomRoundness(
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        sourceType: SourceType,
+    ): Boolean {
+        return requestBottomRoundness(
+            value = value,
+            sourceType = sourceType,
+            animate = roundableState.targetView.isShown
+        )
+    }
+
+    /**
+     * Request the roundness [value] for a specific [sourceType].
+     *
+     * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
+     * more origins require different roundness, for the same property, the maximum value will
+     * always be chosen.
+     *
+     * @param top top value between 0f and 1f.
+     * @param bottom bottom value between 0f and 1f.
+     * @param sourceType the source from which the request for roundness comes.
+     * @param animate true if it should animate to that value.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestRoundness(
+        @FloatRange(from = 0.0, to = 1.0) top: Float,
+        @FloatRange(from = 0.0, to = 1.0) bottom: Float,
+        sourceType: SourceType,
+        animate: Boolean,
+    ): Boolean {
+        val hasTopChanged =
+            requestTopRoundness(value = top, sourceType = sourceType, animate = animate)
+        val hasBottomChanged =
+            requestBottomRoundness(value = bottom, sourceType = sourceType, animate = animate)
+        return hasTopChanged || hasBottomChanged
+    }
+
+    /**
+     * Request the roundness [value] for a specific [sourceType]. Animate the roundness if the view
+     * is shown.
+     *
+     * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
+     * more origins require different roundness, for the same property, the maximum value will
+     * always be chosen.
+     *
+     * @param top top value between 0f and 1f.
+     * @param bottom bottom value between 0f and 1f.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestRoundness(
+        @FloatRange(from = 0.0, to = 1.0) top: Float,
+        @FloatRange(from = 0.0, to = 1.0) bottom: Float,
+        sourceType: SourceType,
+    ): Boolean {
+        return requestRoundness(
+            top = top,
+            bottom = bottom,
+            sourceType = sourceType,
+            animate = roundableState.targetView.isShown,
+        )
+    }
+
+    /**
+     * Request the roundness 0f for a [SourceType]. Animate the roundness if the view is shown.
+     *
+     * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
+     * more origins require different roundness, for the same property, the maximum value will
+     * always be chosen.
+     *
+     * @param sourceType the source from which the request for roundness comes.
+     */
+    @JvmDefault
+    fun requestRoundnessReset(sourceType: SourceType) {
+        requestRoundness(top = 0f, bottom = 0f, sourceType = sourceType)
+    }
+
     /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
     @JvmDefault
-    fun applyRoundness() {
+    fun applyRoundnessAndInvalidate() {
         roundableState.targetView.invalidate()
     }
 
@@ -227,7 +343,7 @@
     /** Set the current top roundness */
     internal fun setTopRoundness(
         value: Float,
-        animated: Boolean = targetView.isShown,
+        animated: Boolean,
     ) {
         PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
     }
@@ -235,11 +351,19 @@
     /** Set the current bottom roundness */
     internal fun setBottomRoundness(
         value: Float,
-        animated: Boolean = targetView.isShown,
+        animated: Boolean,
     ) {
         PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
     }
 
+    fun debugString() = buildString {
+        append("TargetView: ${targetView.hashCode()} ")
+        append("Top: $topRoundness ")
+        append(topRoundnessMap.map { "${it.key} ${it.value}" })
+        append(" Bottom: $bottomRoundness ")
+        append(bottomRoundnessMap.map { "${it.key} ${it.value}" })
+    }
+
     companion object {
         private val DURATION: AnimationProperties =
             AnimationProperties()
@@ -252,7 +376,7 @@
 
                     override fun setValue(view: View, value: Float) {
                         roundable.roundableState.topRoundness = value
-                        roundable.applyRoundness()
+                        roundable.applyRoundnessAndInvalidate()
                     }
                 },
                 R.id.top_roundess_animator_tag,
@@ -267,7 +391,7 @@
 
                     override fun setValue(view: View, value: Float) {
                         roundable.roundableState.bottomRoundness = value
-                        roundable.applyRoundness()
+                        roundable.applyRoundnessAndInvalidate()
                     }
                 },
                 R.id.bottom_roundess_animator_tag,
@@ -277,7 +401,31 @@
     }
 }
 
-enum class SourceType {
+/**
+ * Interface used to define the owner of a roundness. Usually the [SourceType] is defined as a
+ * private property of a class.
+ */
+interface SourceType {
+    companion object {
+        /**
+         * This is the most convenient way to define a new [SourceType].
+         *
+         * For example:
+         *
+         * ```kotlin
+         *     private val SECTION = SourceType.from("Section")
+         * ```
+         */
+        @JvmStatic
+        fun from(name: String) =
+            object : SourceType {
+                override fun toString() = name
+            }
+    }
+}
+
+@Deprecated("Use SourceType.from() instead", ReplaceWith("SourceType.from()"))
+enum class LegacySourceType : SourceType {
     DefaultValue,
     OnDismissAnimation,
     OnScroll,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index d29298a..fbe88df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -39,9 +39,13 @@
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.notification.FakeShadowView;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf}
  * to implement dimming/activating on Keyguard for the double-tap gesture
@@ -91,6 +95,7 @@
             = new PathInterpolator(0.6f, 0, 0.5f, 1);
     private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
             = new PathInterpolator(0, 0, 0.5f, 1);
+    private final Set<SourceType> mOnDetachResetRoundness = new HashSet<>();
     private int mTintedRippleColor;
     private int mNormalRippleColor;
     private Gefingerpoken mTouchHandler;
@@ -134,6 +139,7 @@
     private boolean mDismissed;
     private boolean mRefocusOnDismiss;
     private AccessibilityManager mAccessibilityManager;
+    protected boolean mUseRoundnessSourceTypes;
 
     public ActivatableNotificationView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -613,9 +619,9 @@
     protected void resetAllContentAlphas() {}
 
     @Override
-    public void applyRoundness() {
-        super.applyRoundness();
+    public void applyRoundnessAndInvalidate() {
         applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius());
+        super.applyRoundnessAndInvalidate();
     }
 
     @Override
@@ -775,6 +781,33 @@
         mAccessibilityManager = accessibilityManager;
     }
 
+    /**
+     * Enable the support for rounded corner based on the SourceType
+     * @param enabled true if is supported
+     */
+    public void useRoundnessSourceTypes(boolean enabled) {
+        mUseRoundnessSourceTypes = enabled;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mUseRoundnessSourceTypes && !mOnDetachResetRoundness.isEmpty()) {
+            for (SourceType sourceType : mOnDetachResetRoundness) {
+                requestRoundnessReset(sourceType);
+            }
+            mOnDetachResetRoundness.clear();
+        }
+    }
+
+    /**
+     * SourceType which should be reset when this View is detached
+     * @param sourceType will be reset on View detached
+     */
+    public void addOnDetachResetRoundness(SourceType sourceType) {
+        mOnDetachResetRoundness.add(sourceType);
+    }
+
     public interface OnActivatedListener {
         void onActivated(ActivatableNotificationView view);
         void onActivationReset(ActivatableNotificationView view);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index d7d5ac9..c7c1634 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -91,6 +91,7 @@
 import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
 import com.android.systemui.statusbar.notification.FeedbackIcon;
 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
+import com.android.systemui.statusbar.notification.LegacySourceType;
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
 import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -143,6 +144,9 @@
     private static final int MENU_VIEW_INDEX = 0;
     public static final float DEFAULT_HEADER_VISIBLE_AMOUNT = 1.0f;
     private static final long RECENTLY_ALERTED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30);
+    private static final SourceType BASE_VALUE = SourceType.from("BaseValue");
+    private static final SourceType FROM_PARENT = SourceType.from("FromParent(ENR)");
+    private static final SourceType PINNED = SourceType.from("Pinned");
 
     // We don't correctly track dark mode until the content views are inflated, so always update
     // the background on first content update just in case it happens to be during a theme change.
@@ -150,6 +154,7 @@
     private boolean mNotificationTranslationFinished = false;
     private boolean mIsSnoozed;
     private boolean mIsFaded;
+    private boolean mAnimatePinnedRoundness = false;
 
     /**
      * Listener for when {@link ExpandableNotificationRow} is laid out.
@@ -376,7 +381,7 @@
 
     private float mTopRoundnessDuringLaunchAnimation;
     private float mBottomRoundnessDuringLaunchAnimation;
-    private boolean mIsNotificationGroupCornerEnabled;
+    private float mSmallRoundness;
 
     /**
      * Returns whether the given {@code statusBarNotification} is a system notification.
@@ -844,7 +849,9 @@
         }
         onAttachedChildrenCountChanged();
         row.setIsChildInGroup(false, null);
-        row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
+        if (!mUseRoundnessSourceTypes) {
+            row.requestBottomRoundness(0.0f, LegacySourceType.DefaultValue, /* animate = */ false);
+        }
     }
 
     /**
@@ -860,7 +867,10 @@
             if (child.keepInParentForDismissAnimation()) {
                 mChildrenContainer.removeNotification(child);
                 child.setIsChildInGroup(false, null);
-                child.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
+                if (!mUseRoundnessSourceTypes) {
+                    LegacySourceType sourceType = LegacySourceType.DefaultValue;
+                    child.requestBottomRoundness(0f, sourceType, /* animate = */ false);
+                }
                 child.setKeepInParentForDismissAnimation(false);
                 logKeepInParentChildDetached(child);
                 childCountChanged = true;
@@ -915,6 +925,9 @@
             mNotificationParent.updateBackgroundForGroupState();
         }
         updateBackgroundClipping();
+        if (mUseRoundnessSourceTypes) {
+            updateBaseRoundness();
+        }
     }
 
     @Override
@@ -1033,6 +1046,16 @@
         if (isAboveShelf() != wasAboveShelf) {
             mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
         }
+        if (mUseRoundnessSourceTypes) {
+            if (pinned) {
+                // Should be animated if someone explicitly set it to 0 and the row is shown.
+                boolean animated = mAnimatePinnedRoundness && isShown();
+                requestRoundness(/* top = */ 1f, /* bottom = */ 1f, PINNED, animated);
+            } else {
+                requestRoundnessReset(PINNED);
+                mAnimatePinnedRoundness = true;
+            }
+        }
     }
 
     @Override
@@ -1607,6 +1630,8 @@
         super(context, attrs);
         mImageResolver = new NotificationInlineImageResolver(context,
                 new NotificationInlineImageCache());
+        float radius = getResources().getDimension(R.dimen.notification_corner_radius_small);
+        mSmallRoundness = radius / getMaxRadius();
         initDimens();
     }
 
@@ -1839,7 +1864,7 @@
             mChildrenContainer.setIsLowPriority(mIsLowPriority);
             mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
             mChildrenContainer.onNotificationUpdated();
-            mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
+            mChildrenContainer.useRoundnessSourceTypes(mUseRoundnessSourceTypes);
 
             mTranslateableViews.add(mChildrenContainer);
         });
@@ -2271,7 +2296,7 @@
 
     @Override
     public float getTopRoundness() {
-        if (mExpandAnimationRunning) {
+        if (!mUseRoundnessSourceTypes && mExpandAnimationRunning) {
             return mTopRoundnessDuringLaunchAnimation;
         }
 
@@ -2280,7 +2305,7 @@
 
     @Override
     public float getBottomRoundness() {
-        if (mExpandAnimationRunning) {
+        if (!mUseRoundnessSourceTypes && mExpandAnimationRunning) {
             return mBottomRoundnessDuringLaunchAnimation;
         }
 
@@ -3436,17 +3461,24 @@
     }
 
     @Override
-    public void applyRoundness() {
-        super.applyRoundness();
+    public void applyRoundnessAndInvalidate() {
         applyChildrenRoundness();
+        super.applyRoundnessAndInvalidate();
     }
 
     private void applyChildrenRoundness() {
         if (mIsSummaryWithChildren) {
-            mChildrenContainer.requestBottomRoundness(
-                    getBottomRoundness(),
-                    /* animate = */ false,
-                    SourceType.DefaultValue);
+            if (mUseRoundnessSourceTypes) {
+                mChildrenContainer.requestRoundness(
+                        /* top = */ getTopRoundness(),
+                        /* bottom = */ getBottomRoundness(),
+                        FROM_PARENT);
+            } else {
+                mChildrenContainer.requestBottomRoundness(
+                        getBottomRoundness(),
+                        LegacySourceType.DefaultValue,
+                        /* animate = */ false);
+            }
         }
     }
 
@@ -3605,6 +3637,7 @@
             } else {
                 pw.println("no viewState!!!");
             }
+            pw.println("Roundness: " + getRoundableState().debugString());
 
             if (mIsSummaryWithChildren) {
                 pw.println();
@@ -3649,14 +3682,38 @@
         return mTargetPoint;
     }
 
+    /** Update the minimum roundness based on current state */
+    private void updateBaseRoundness() {
+        if (isChildInGroup()) {
+            requestRoundnessReset(BASE_VALUE);
+        } else {
+            requestRoundness(mSmallRoundness, mSmallRoundness, BASE_VALUE);
+        }
+    }
+
     /**
-     * Enable the support for rounded corner in notification group
+     * Enable the support for rounded corner based on the SourceType
      * @param enabled true if is supported
      */
-    public void enableNotificationGroupCorner(boolean enabled) {
-        mIsNotificationGroupCornerEnabled = enabled;
+    @Override
+    public void useRoundnessSourceTypes(boolean enabled) {
+        super.useRoundnessSourceTypes(enabled);
         if (mChildrenContainer != null) {
-            mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
+            mChildrenContainer.useRoundnessSourceTypes(mUseRoundnessSourceTypes);
+        }
+    }
+
+    @Override
+    public String toString() {
+        String roundableStateDebug = "RoundableState = " + getRoundableState().debugString();
+        return "ExpandableNotificationRow:" + hashCode() + " { " + roundableStateDebug + " }";
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (mUseRoundnessSourceTypes) {
+            updateBaseRoundness();
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index 8a400d5..d113860 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -255,8 +255,8 @@
                 mStatusBarStateController.removeCallback(mStatusBarStateListener);
             }
         });
-        mView.enableNotificationGroupCorner(
-                mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER));
+        mView.useRoundnessSourceTypes(
+                mFeatureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES));
     }
 
     private final StatusBarStateController.StateListener mStatusBarStateListener =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 2324627..0213b96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -219,9 +219,9 @@
     }
 
     @Override
-    public void applyRoundness() {
+    public void applyRoundnessAndInvalidate() {
         invalidateOutline();
-        super.applyRoundness();
+        super.applyRoundnessAndInvalidate();
     }
 
     protected void setBackgroundTop(int backgroundTop) {
@@ -233,7 +233,7 @@
 
     public void onDensityOrFontScaleChanged() {
         initDimens();
-        applyRoundness();
+        applyRoundnessAndInvalidate();
     }
 
     @Override
@@ -241,7 +241,7 @@
         int previousHeight = getActualHeight();
         super.setActualHeight(actualHeight, notifyListeners);
         if (previousHeight != actualHeight) {
-            applyRoundness();
+            applyRoundnessAndInvalidate();
         }
     }
 
@@ -250,7 +250,7 @@
         int previousAmount = getClipTopAmount();
         super.setClipTopAmount(clipTopAmount);
         if (previousAmount != clipTopAmount) {
-            applyRoundness();
+            applyRoundnessAndInvalidate();
         }
     }
 
@@ -259,14 +259,14 @@
         int previousAmount = getClipBottomAmount();
         super.setClipBottomAmount(clipBottomAmount);
         if (previousAmount != clipBottomAmount) {
-            applyRoundness();
+            applyRoundnessAndInvalidate();
         }
     }
 
     protected void setOutlineAlpha(float alpha) {
         if (alpha != mOutlineAlpha) {
             mOutlineAlpha = alpha;
-            applyRoundness();
+            applyRoundnessAndInvalidate();
         }
     }
 
@@ -280,7 +280,7 @@
             setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
         } else {
             mCustomOutline = false;
-            applyRoundness();
+            applyRoundnessAndInvalidate();
         }
     }
 
@@ -340,7 +340,7 @@
         // Outlines need to be at least 1 dp
         mOutlineRect.bottom = (int) Math.max(top, mOutlineRect.bottom);
         mOutlineRect.right = (int) Math.max(left, mOutlineRect.right);
-        applyRoundness();
+        applyRoundnessAndInvalidate();
     }
 
     public Path getCustomClipPath(View child) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index f13e48d..1f664cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -71,6 +71,8 @@
     private View mFeedbackIcon;
     private boolean mIsLowPriority;
     private boolean mTransformLowPriorityTitle;
+    private boolean mUseRoundnessSourceTypes;
+    private RoundnessChangedListener mRoundnessChangedListener;
 
     protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
         super(ctx, view, row);
@@ -117,6 +119,20 @@
         return mRoundableState;
     }
 
+    @Override
+    public void applyRoundnessAndInvalidate() {
+        if (mUseRoundnessSourceTypes && mRoundnessChangedListener != null) {
+            // We cannot apply the rounded corner to this View, so our parents (in drawChild()) will
+            // clip our canvas. So we should invalidate our parent.
+            mRoundnessChangedListener.applyRoundnessAndInvalidate();
+        }
+        Roundable.super.applyRoundnessAndInvalidate();
+    }
+
+    public void setOnRoundnessChangedListener(RoundnessChangedListener listener) {
+        mRoundnessChangedListener = listener;
+    }
+
     protected void resolveHeaderViews() {
         mIcon = mView.findViewById(com.android.internal.R.id.icon);
         mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
@@ -343,4 +359,23 @@
             }
         }
     }
+
+    /**
+     * Enable the support for rounded corner based on the SourceType
+     *
+     * @param enabled true if is supported
+     */
+    public void useRoundnessSourceTypes(boolean enabled) {
+        mUseRoundnessSourceTypes = enabled;
+    }
+
+    /**
+     * Interface that handle the Roundness changes
+     */
+    public interface RoundnessChangedListener {
+        /**
+         * This method will be called when this class call applyRoundnessAndInvalidate()
+         */
+        void applyRoundnessAndInvalidate();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index d43ca823..4a8e2db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -47,6 +47,7 @@
 import com.android.systemui.statusbar.NotificationGroupingUtil;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.notification.FeedbackIcon;
+import com.android.systemui.statusbar.notification.LegacySourceType;
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.Roundable;
@@ -83,6 +84,7 @@
             return mAnimationFilter;
         }
     }.setDuration(200);
+    private static final SourceType FROM_PARENT = SourceType.from("FromParent(NCC)");
 
     private final List<View> mDividers = new ArrayList<>();
     private final List<ExpandableNotificationRow> mAttachedChildren = new ArrayList<>();
@@ -131,7 +133,7 @@
     private int mUntruncatedChildCount;
     private boolean mContainingNotificationIsFaded = false;
     private RoundableState mRoundableState;
-    private boolean mIsNotificationGroupCornerEnabled;
+    private boolean mUseRoundnessSourceTypes;
 
     public NotificationChildrenContainer(Context context) {
         this(context, null);
@@ -313,9 +315,12 @@
         row.setContentTransformationAmount(0, false /* isLastChild */);
         row.setNotificationFaded(mContainingNotificationIsFaded);
 
-        // This is a workaround, the NotificationShelf should be the owner of `OnScroll` roundness.
-        // Here we should reset the `OnScroll` roundness only on top-level rows.
-        NotificationShelf.resetOnScrollRoundness(row);
+        if (!mUseRoundnessSourceTypes) {
+            // This is a workaround, the NotificationShelf should be the owner of `OnScroll`
+            // roundness.
+            // Here we should reset the `OnScroll` roundness only on top-level rows.
+            NotificationShelf.resetLegacyOnScrollRoundness(row);
+        }
 
         // It doesn't make sense to keep old animations around, lets cancel them!
         ExpandableViewState viewState = row.getViewState();
@@ -323,6 +328,10 @@
             viewState.cancelAnimations(row);
             row.cancelAppearDrawing();
         }
+
+        if (mUseRoundnessSourceTypes) {
+            applyRoundnessAndInvalidate();
+        }
     }
 
     private void ensureRemovedFromTransientContainer(View v) {
@@ -356,6 +365,11 @@
         if (!row.isRemoved()) {
             mGroupingUtil.restoreChildNotification(row);
         }
+
+        if (mUseRoundnessSourceTypes) {
+            row.requestRoundnessReset(FROM_PARENT);
+            applyRoundnessAndInvalidate();
+        }
     }
 
     /**
@@ -382,6 +396,10 @@
                             getContext(),
                             mNotificationHeader,
                             mContainingNotification);
+            mNotificationHeaderWrapper.useRoundnessSourceTypes(mUseRoundnessSourceTypes);
+            if (mUseRoundnessSourceTypes) {
+                mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+            }
             addView(mNotificationHeader, 0);
             invalidate();
         } else {
@@ -419,6 +437,12 @@
                                 getContext(),
                                 mNotificationHeaderLowPriority,
                                 mContainingNotification);
+                mNotificationHeaderWrapperLowPriority.useRoundnessSourceTypes(
+                        mUseRoundnessSourceTypes
+                );
+                if (mUseRoundnessSourceTypes) {
+                    mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+                }
                 addView(mNotificationHeaderLowPriority, 0);
                 invalidate();
             } else {
@@ -841,7 +865,7 @@
 
             isCanvasChanged = true;
             canvas.save();
-            if (mIsNotificationGroupCornerEnabled && translation != 0f) {
+            if (mUseRoundnessSourceTypes && translation != 0f) {
                 clipPath.offset(translation, 0f);
                 canvas.clipPath(clipPath);
                 clipPath.offset(-translation, 0f);
@@ -1392,24 +1416,28 @@
     }
 
     @Override
-    public void applyRoundness() {
-        Roundable.super.applyRoundness();
+    public void applyRoundnessAndInvalidate() {
         boolean last = true;
         for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
             ExpandableNotificationRow child = mAttachedChildren.get(i);
             if (child.getVisibility() == View.GONE) {
                 continue;
             }
-            child.requestTopRoundness(
-                    /* value = */ 0f,
-                    /* animate = */ isShown(),
-                    SourceType.DefaultValue);
-            child.requestBottomRoundness(
-                    /* value = */ last ? getBottomRoundness() : 0f,
-                    /* animate = */ isShown(),
-                    SourceType.DefaultValue);
+            if (mUseRoundnessSourceTypes) {
+                child.requestRoundness(
+                        /* top = */ 0f,
+                        /* bottom = */ last ? getBottomRoundness() : 0f,
+                        FROM_PARENT);
+            } else {
+                child.requestRoundness(
+                        /* top = */ 0f,
+                        /* bottom = */ last ? getBottomRoundness() : 0f,
+                        LegacySourceType.DefaultValue,
+                        /* animate = */ isShown());
+            }
             last = false;
         }
+        Roundable.super.applyRoundnessAndInvalidate();
     }
 
     public void setHeaderVisibleAmount(float headerVisibleAmount) {
@@ -1467,10 +1495,17 @@
     }
 
     /**
-     * Enable the support for rounded corner in notification group
+     * Enable the support for rounded corner based on the SourceType
+     *
      * @param enabled true if is supported
      */
-    public void enableNotificationGroupCorner(boolean enabled) {
-        mIsNotificationGroupCornerEnabled = enabled;
+    public void useRoundnessSourceTypes(boolean enabled) {
+        mUseRoundnessSourceTypes = enabled;
+    }
+
+    @Override
+    public String toString() {
+        String roundableStateDebug = "RoundableState = " + getRoundableState().debugString();
+        return "NotificationChildrenContainer:" + hashCode() + " { " + roundableStateDebug + " }";
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
index 6810055..fde8c4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
@@ -25,6 +25,9 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.statusbar.notification.LegacySourceType;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
 import com.android.systemui.statusbar.notification.Roundable;
 import com.android.systemui.statusbar.notification.SourceType;
@@ -45,6 +48,7 @@
 public class NotificationRoundnessManager implements Dumpable {
 
     private static final String TAG = "NotificationRoundnessManager";
+    private static final SourceType DISMISS_ANIMATION = SourceType.from("DismissAnimation");
 
     private final ExpandableView[] mFirstInSectionViews;
     private final ExpandableView[] mLastInSectionViews;
@@ -63,12 +67,14 @@
     private ExpandableView mSwipedView = null;
     private Roundable mViewBeforeSwipedView = null;
     private Roundable mViewAfterSwipedView = null;
+    private boolean mUseRoundnessSourceTypes;
 
     @Inject
     NotificationRoundnessManager(
             NotificationSectionsFeatureManager sectionsFeatureManager,
             NotificationRoundnessLogger notifLogger,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            FeatureFlags featureFlags) {
         int numberOfSections = sectionsFeatureManager.getNumberOfBuckets();
         mFirstInSectionViews = new ExpandableView[numberOfSections];
         mLastInSectionViews = new ExpandableView[numberOfSections];
@@ -76,6 +82,7 @@
         mTmpLastInSectionViews = new ExpandableView[numberOfSections];
         mNotifLogger = notifLogger;
         mDumpManager = dumpManager;
+        mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
 
         mDumpManager.registerDumpable(TAG, this);
     }
@@ -94,6 +101,7 @@
     }
 
     public void updateView(ExpandableView view, boolean animate) {
+        if (mUseRoundnessSourceTypes) return;
         boolean changed = updateViewWithoutCallback(view, animate);
         if (changed) {
             mRoundingChangedCallback.run();
@@ -110,6 +118,7 @@
     boolean updateViewWithoutCallback(
             ExpandableView view,
             boolean animate) {
+        if (mUseRoundnessSourceTypes) return false;
         if (view == null
                 || view == mViewBeforeSwipedView
                 || view == mViewAfterSwipedView) {
@@ -118,13 +127,13 @@
 
         final boolean isTopChanged = view.requestTopRoundness(
                 getRoundnessDefaultValue(view, true /* top */),
-                animate,
-                SourceType.DefaultValue);
+                LegacySourceType.DefaultValue,
+                animate);
 
         final boolean isBottomChanged = view.requestBottomRoundness(
                 getRoundnessDefaultValue(view, /* top = */ false),
-                animate,
-                SourceType.DefaultValue);
+                LegacySourceType.DefaultValue,
+                animate);
 
         final boolean isFirstInSection = isFirstInSection(view);
         final boolean isLastInSection = isLastInSection(view);
@@ -139,6 +148,7 @@
     }
 
     private boolean isFirstInSection(ExpandableView view) {
+        if (mUseRoundnessSourceTypes) return false;
         for (int i = 0; i < mFirstInSectionViews.length; i++) {
             if (view == mFirstInSectionViews[i]) {
                 return true;
@@ -148,6 +158,7 @@
     }
 
     private boolean isLastInSection(ExpandableView view) {
+        if (mUseRoundnessSourceTypes) return false;
         for (int i = mLastInSectionViews.length - 1; i >= 0; i--) {
             if (view == mLastInSectionViews[i]) {
                 return true;
@@ -160,9 +171,6 @@
             Roundable viewBefore,
             ExpandableView viewSwiped,
             Roundable viewAfter) {
-        final boolean animate = true;
-        final SourceType source = SourceType.OnDismissAnimation;
-
         // This method requires you to change the roundness of the current View targets and reset
         // the roundness of the old View targets (if any) to 0f.
         // To avoid conflicts, it generates a set of old Views and removes the current Views
@@ -172,31 +180,34 @@
         if (mSwipedView != null) oldViews.add(mSwipedView);
         if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView);
 
+        final SourceType source;
+        if (mUseRoundnessSourceTypes) {
+            source = DISMISS_ANIMATION;
+        } else {
+            source = LegacySourceType.OnDismissAnimation;
+        }
+
         mViewBeforeSwipedView = viewBefore;
         if (viewBefore != null) {
             oldViews.remove(viewBefore);
-            viewBefore.requestTopRoundness(0f, animate, source);
-            viewBefore.requestBottomRoundness(1f, animate, source);
+            viewBefore.requestRoundness(/* top = */ 0f, /* bottom = */ 1f, source);
         }
 
         mSwipedView = viewSwiped;
         if (viewSwiped != null) {
             oldViews.remove(viewSwiped);
-            viewSwiped.requestTopRoundness(1f, animate, source);
-            viewSwiped.requestBottomRoundness(1f, animate, source);
+            viewSwiped.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, source);
         }
 
         mViewAfterSwipedView = viewAfter;
         if (viewAfter != null) {
             oldViews.remove(viewAfter);
-            viewAfter.requestTopRoundness(1f, animate, source);
-            viewAfter.requestBottomRoundness(0f, animate, source);
+            viewAfter.requestRoundness(/* top = */ 1f, /* bottom = */ 0f, source);
         }
 
         // After setting the current Views, reset the views that are still present in the set.
         for (Roundable oldView : oldViews) {
-            oldView.requestTopRoundness(0f, animate, source);
-            oldView.requestBottomRoundness(0f, animate, source);
+            oldView.requestRoundnessReset(source);
         }
     }
 
@@ -204,7 +215,23 @@
         mIsClearAllInProgress = isClearingAll;
     }
 
+    /**
+     * Check if "Clear all" notifications is in progress.
+     */
+    public boolean isClearAllInProgress() {
+        return mIsClearAllInProgress;
+    }
+
+    /**
+     * Check if we can request the `Pulsing` roundness for notification.
+     */
+    public boolean shouldRoundNotificationPulsing() {
+        return mRoundForPulsingViews;
+    }
+
     private float getRoundnessDefaultValue(Roundable view, boolean top) {
+        if (mUseRoundnessSourceTypes) return 0f;
+
         if (view == null) {
             return 0f;
         }
@@ -250,6 +277,7 @@
     }
 
     public void setExpanded(float expandedHeight, float appearFraction) {
+        if (mUseRoundnessSourceTypes) return;
         mExpanded = expandedHeight != 0.0f;
         mAppearFraction = appearFraction;
         if (mTrackedHeadsUp != null) {
@@ -258,6 +286,7 @@
     }
 
     public void updateRoundedChildren(NotificationSection[] sections) {
+        if (mUseRoundnessSourceTypes) return;
         boolean anyChanged = false;
         for (int i = 0; i < sections.length; i++) {
             mTmpFirstInSectionViews[i] = mFirstInSectionViews[i];
@@ -280,6 +309,7 @@
             NotificationSection[] sections,
             ExpandableView[] oldViews,
             boolean first) {
+        if (mUseRoundnessSourceTypes) return false;
         boolean anyChanged = false;
         for (ExpandableView oldView : oldViews) {
             if (oldView != null) {
@@ -313,6 +343,7 @@
             NotificationSection[] sections,
             ExpandableView[] oldViews,
             boolean first) {
+        if (mUseRoundnessSourceTypes) return false;
         boolean anyChanged = false;
         for (NotificationSection section : sections) {
             ExpandableView newView =
@@ -339,6 +370,15 @@
         mAnimatedChildren = animatedChildren;
     }
 
+    /**
+     * Check if the view should be animated
+     * @param view target view
+     * @return true, if is in the AnimatedChildren set
+     */
+    public boolean isAnimatedChild(ExpandableView view) {
+        return mAnimatedChildren.contains(view);
+    }
+
     public void setOnRoundingChangedCallback(Runnable roundingChangedCallback) {
         mRoundingChangedCallback = roundingChangedCallback;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index a1b77ac..070b439 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -19,8 +19,11 @@
 import android.util.Log
 import android.view.View
 import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.media.controls.ui.KeyguardMediaController
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
+import com.android.systemui.statusbar.notification.SourceType
 import com.android.systemui.statusbar.notification.collection.render.MediaContainerController
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
 import com.android.systemui.statusbar.notification.dagger.AlertingHeader
@@ -44,12 +47,16 @@
     private val keyguardMediaController: KeyguardMediaController,
     private val sectionsFeatureManager: NotificationSectionsFeatureManager,
     private val mediaContainerController: MediaContainerController,
+    private val notificationRoundnessManager: NotificationRoundnessManager,
     @IncomingHeader private val incomingHeaderController: SectionHeaderController,
     @PeopleHeader private val peopleHeaderController: SectionHeaderController,
     @AlertingHeader private val alertingHeaderController: SectionHeaderController,
-    @SilentHeader private val silentHeaderController: SectionHeaderController
+    @SilentHeader private val silentHeaderController: SectionHeaderController,
+    featureFlags: FeatureFlags
 ) : SectionProvider {
 
+    private val useRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES)
+
     private val configurationListener = object : ConfigurationController.ConfigurationListener {
         override fun onLocaleListChanged() {
             reinflateViews()
@@ -177,11 +184,49 @@
                         size = sections.size,
                         operation = SectionBounds::addNotif
                 )
+
+        // Build a set of the old first/last Views of the sections
+        val oldFirstChildren = sections.mapNotNull { it.firstVisibleChild }.toSet().toMutableSet()
+        val oldLastChildren = sections.mapNotNull { it.lastVisibleChild }.toSet().toMutableSet()
+
         // Update each section with the associated boundary, tracking if there was a change
         val changed = sections.fold(false) { changed, section ->
             val bounds = sectionBounds[section.bucket] ?: SectionBounds.None
-            bounds.updateSection(section) || changed
+            val isSectionChanged = bounds.updateSection(section)
+            isSectionChanged || changed
         }
+
+        if (useRoundnessSourceTypes) {
+            val newFirstChildren = sections.mapNotNull { it.firstVisibleChild }
+            val newLastChildren = sections.mapNotNull { it.lastVisibleChild }
+
+            // Update the roundness of Views that weren't already in the first/last position
+            newFirstChildren.forEach { firstChild ->
+                val wasFirstChild = oldFirstChildren.remove(firstChild)
+                if (!wasFirstChild) {
+                    val notAnimatedChild = !notificationRoundnessManager.isAnimatedChild(firstChild)
+                    val animated = firstChild.isShown && notAnimatedChild
+                    firstChild.requestTopRoundness(1f, SECTION, animated)
+                }
+            }
+            newLastChildren.forEach { lastChild ->
+                val wasLastChild = oldLastChildren.remove(lastChild)
+                if (!wasLastChild) {
+                    val notAnimatedChild = !notificationRoundnessManager.isAnimatedChild(lastChild)
+                    val animated = lastChild.isShown && notAnimatedChild
+                    lastChild.requestBottomRoundness(1f, SECTION, animated)
+                }
+            }
+
+            // The Views left in the set are no longer in the first/last position
+            oldFirstChildren.forEach { noMoreFirstChild ->
+                noMoreFirstChild.requestTopRoundness(0f, SECTION)
+            }
+            oldLastChildren.forEach { noMoreLastChild ->
+                noMoreLastChild.requestBottomRoundness(0f, SECTION)
+            }
+        }
+
         if (DEBUG) {
             logSections(sections)
         }
@@ -215,5 +260,6 @@
     companion object {
         private const val TAG = "NotifSectionsManager"
         private const val DEBUG = false
+        private val SECTION = SourceType.from("Section")
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 7c3e52c..21e2bd8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -191,10 +191,13 @@
 
     private final boolean mDebugLines;
     private Paint mDebugPaint;
-    /** Used to track the Y positions that were already used to draw debug text labels. */
+    /**
+     * Used to track the Y positions that were already used to draw debug text labels.
+     */
     private Set<Integer> mDebugTextUsedYPositions;
     private final boolean mDebugRemoveAnimation;
     private final boolean mSimplifiedAppearFraction;
+    private final boolean mUseRoundnessSourceTypes;
 
     private int mContentHeight;
     private float mIntrinsicContentHeight;
@@ -355,15 +358,14 @@
     private final Rect mQsHeaderBound = new Rect();
     private boolean mContinuousShadowUpdate;
     private boolean mContinuousBackgroundUpdate;
-    private final ViewTreeObserver.OnPreDrawListener mShadowUpdater
-            = () -> {
-                updateViewShadows();
-                return true;
-            };
+    private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> {
+        updateViewShadows();
+        return true;
+    };
     private final ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> {
-                updateBackground();
-                return true;
-            };
+        updateBackground();
+        return true;
+    };
     private final Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> {
         float endY = view.getTranslationY() + view.getActualHeight();
         float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
@@ -419,7 +421,8 @@
      */
     private int mMaxDisplayedNotifications = -1;
     private float mKeyguardBottomPadding = -1;
-    @VisibleForTesting int mStatusBarHeight;
+    @VisibleForTesting
+    int mStatusBarHeight;
     private int mMinInteractionHeight;
     private final Rect mClipRect = new Rect();
     private boolean mIsClipped;
@@ -568,6 +571,8 @@
 
     @Nullable
     private OnClickListener mManageButtonClickListener;
+    @Nullable
+    private OnNotificationRemovedListener mOnNotificationRemovedListener;
 
     public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
         super(context, attrs, 0, 0);
@@ -576,6 +581,7 @@
         mDebugLines = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES);
         mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION);
         mSimplifiedAppearFraction = featureFlags.isEnabled(Flags.SIMPLIFIED_APPEAR_FRACTION);
+        mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
         mSectionsManager = Dependency.get(NotificationSectionsManager.class);
         mScreenOffAnimationController =
                 Dependency.get(ScreenOffAnimationController.class);
@@ -741,7 +747,7 @@
     }
 
     private void logHunSkippedForUnexpectedState(ExpandableNotificationRow enr,
-            boolean expected, boolean actual) {
+                                                 boolean expected, boolean actual) {
         if (mLogger == null) return;
         mLogger.hunSkippedForUnexpectedState(enr.getEntry(), expected, actual);
     }
@@ -868,13 +874,13 @@
 
     /**
      * Draws round rects for each background section.
-     *
+     * <p>
      * We want to draw a round rect for each background section as defined by {@link #mSections}.
      * However, if two sections are directly adjacent with no gap between them (e.g. on the
      * lockscreen where the shelf can appear directly below the high priority section, or while
      * scrolling the shade so that the top of the shelf is right at the bottom of the high priority
      * section), we don't want to round the adjacent corners.
-     *
+     * <p>
      * Since {@link Canvas} doesn't provide a way to draw a half-rounded rect, this means that we
      * need to coalesce the backgrounds for adjacent sections and draw them as a single round rect.
      * This method tracks the top of each rect we need to draw, then iterates through the visible
@@ -883,7 +889,7 @@
      * the current section.  When we're done iterating we will always have one rect left to draw.
      */
     private void drawBackgroundRects(Canvas canvas, int left, int right, int top,
-            int animationYOffset) {
+                                     int animationYOffset) {
         int backgroundRectTop = top;
         int lastSectionBottom =
                 mSections[0].getCurrentBounds().bottom + animationYOffset;
@@ -974,7 +980,7 @@
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     void initView(Context context, NotificationSwipeHelper swipeHelper,
-            NotificationStackSizeCalculator notificationStackSizeCalculator) {
+                  NotificationStackSizeCalculator notificationStackSizeCalculator) {
         mScroller = new OverScroller(getContext());
         mSwipeHelper = swipeHelper;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
@@ -1304,18 +1310,19 @@
     /**
      * @return Whether we should skip stack height updates.
      * True when
-     *      1) Unlock hint is running
-     *      2) Swiping up on lockscreen or flinging down after swipe up
+     * 1) Unlock hint is running
+     * 2) Swiping up on lockscreen or flinging down after swipe up
      */
     private boolean shouldSkipHeightUpdate() {
         return mAmbientState.isOnKeyguard()
                 && (mAmbientState.isUnlockHintRunning()
-                        || mAmbientState.isSwipingUp()
-                        || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
+                || mAmbientState.isSwipingUp()
+                || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
     }
 
     /**
      * Apply expansion fraction to the y position and height of the notifications panel.
+     *
      * @param listenerNeedsAnimation does the listener need to animate?
      */
     private void updateStackPosition(boolean listenerNeedsAnimation) {
@@ -1708,7 +1715,7 @@
      */
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     ExpandableView getChildAtPosition(float touchX, float touchY,
-            boolean requireMinHeight, boolean ignoreDecors) {
+                                      boolean requireMinHeight, boolean ignoreDecors) {
         // find the view under the pointer, accounting for GONE views
         final int count = getChildCount();
         for (int childIdx = 0; childIdx < count; childIdx++) {
@@ -1868,9 +1875,9 @@
     public void dismissViewAnimated(
             View child, Consumer<Boolean> endRunnable, int delay, long duration) {
         if (child instanceof SectionHeaderView) {
-             ((StackScrollerDecorView) child).setContentVisible(
-                     false /* visible */, true /* animate */, endRunnable);
-             return;
+            ((StackScrollerDecorView) child).setContentVisible(
+                    false /* visible */, true /* animate */, endRunnable);
+            return;
         }
         mSwipeHelper.dismissChild(
                 child,
@@ -2032,10 +2039,11 @@
     /**
      * Scrolls by the given delta, overscrolling if needed.  If called during a fling and the delta
      * would cause us to exceed the provided maximum overscroll, springs back instead.
-     *
+     * <p>
      * This method performs the determination of whether we're exceeding the overscroll and clamps
      * the scroll amount if so.  The actual scrolling/overscrolling happens in
      * {@link #onCustomOverScrolled(int, boolean)}
+     *
      * @param deltaY         The (signed) number of pixels to scroll.
      * @param scrollY        The current scroll position (absolute scrolling only).
      * @param scrollRangeY   The maximum allowable scroll position (absolute scrolling only).
@@ -2098,7 +2106,7 @@
      */
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
-            boolean cancelAnimators) {
+                                    boolean cancelAnimators) {
         setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop));
     }
 
@@ -2114,7 +2122,7 @@
      */
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
-            boolean cancelAnimators, boolean isRubberbanded) {
+                                    boolean cancelAnimators, boolean isRubberbanded) {
         if (cancelAnimators) {
             mStateAnimator.cancelOverScrollAnimators(onTop);
         }
@@ -2123,7 +2131,7 @@
 
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate,
-            boolean isRubberbanded) {
+                                             boolean isRubberbanded) {
         amount = Math.max(0, amount);
         if (animate) {
             mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded);
@@ -2179,7 +2187,7 @@
      * Scrolls to the given position, overscrolling if needed.  If called during a fling and the
      * position exceeds the provided maximum overscroll, springs back instead.
      *
-     * @param scrollY The target scroll position.
+     * @param scrollY  The target scroll position.
      * @param clampedY Whether this value was clamped by the calling method, meaning we've reached
      *                 the overscroll limit.
      */
@@ -2288,8 +2296,8 @@
                 if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
                     List<ExpandableNotificationRow> notificationChildren =
                             row.getAttachedChildren();
-                    for (int childIndex = 0; childIndex < notificationChildren.size();
-                            childIndex++) {
+                    int childrenSize = notificationChildren.size();
+                    for (int childIndex = 0; childIndex < childrenSize; childIndex++) {
                         ExpandableNotificationRow rowChild = notificationChildren.get(childIndex);
                         if (rowChild.getTranslationY() + rowTranslation >= translationY) {
                             return rowChild;
@@ -2365,10 +2373,9 @@
     /**
      * Calculate the gap height between two different views
      *
-     * @param previous the previousView
-     * @param current the currentView
+     * @param previous     the previousView
+     * @param current      the currentView
      * @param visibleIndex the visible index in the list
-     *
      * @return the gap height needed before the current view
      */
     public float calculateGapHeight(
@@ -2376,7 +2383,7 @@
             ExpandableView current,
             int visibleIndex
     ) {
-       return mStackScrollAlgorithm.getGapHeightForChild(mSectionsManager, visibleIndex, current,
+        return mStackScrollAlgorithm.getGapHeightForChild(mSectionsManager, visibleIndex, current,
                 previous, mAmbientState.getFractionToShade(), mAmbientState.isOnKeyguard());
     }
 
@@ -2642,15 +2649,15 @@
         return mScrolledToTopOnFirstDown
                 && !mExpandedInThisMotion
                 && (initialVelocity > mMinimumVelocity
-                        || (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0));
+                || (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0));
     }
 
     /**
      * Updates the top padding of the notifications, taking {@link #getIntrinsicPadding()} into
      * account.
      *
-     * @param qsHeight               the top padding imposed by the quick settings panel
-     * @param animate                whether to animate the change
+     * @param qsHeight the top padding imposed by the quick settings panel
+     * @param animate  whether to animate the change
      */
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     public void updateTopPadding(float qsHeight, boolean animate) {
@@ -2724,21 +2731,34 @@
     }
 
 
-
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void setChildTransferInProgress(boolean childTransferInProgress) {
         Assert.isMainThread();
         mChildTransferInProgress = childTransferInProgress;
     }
 
+    /**
+     * Set the remove notification listener
+     * @param listener callback for notification removed
+     */
+    public void setOnNotificationRemovedListener(OnNotificationRemovedListener listener) {
+        mOnNotificationRemovedListener = listener;
+    }
+
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         // we only call our internal methods if this is actually a removal and not just a
         // notification which becomes a child notification
+        ExpandableView expandableView = (ExpandableView) child;
         if (!mChildTransferInProgress) {
-            onViewRemovedInternal((ExpandableView) child, this);
+            onViewRemovedInternal(expandableView, this);
+        }
+        if (mOnNotificationRemovedListener != null) {
+            mOnNotificationRemovedListener.onNotificationRemoved(
+                    expandableView,
+                    mChildTransferInProgress);
         }
     }
 
@@ -2998,8 +3018,10 @@
             mAnimateNextSectionBoundsChange = false;
         }
         mAmbientState.setLastVisibleBackgroundChild(lastChild);
-        // TODO: Refactor SectionManager and put the RoundnessManager there.
-        mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
+        if (!mUseRoundnessSourceTypes) {
+            // TODO: Refactor SectionManager and put the RoundnessManager there.
+            mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
+        }
         mAnimateBottomOnLayout = false;
         invalidate();
     }
@@ -3206,7 +3228,7 @@
                     // Only animate if we still have pinned heads up, otherwise we just have the
                     // regular collapse animation of the lock screen
                     || (mKeyguardBypassEnabled && onKeyguard()
-                            && mInHeadsUpPinnedMode);
+                    && mInHeadsUpPinnedMode);
             if (performDisappearAnimation && !isHeadsUp) {
                 type = row.wasJustClicked()
                         ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
@@ -3351,7 +3373,7 @@
             AnimationEvent animEvent = duration == null
                     ? new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)
                     : new AnimationEvent(
-                            child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION, duration);
+                    child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION, duration);
             mAnimationEvents.add(animEvent);
         }
         mChildrenChangingPositions.clear();
@@ -3442,7 +3464,9 @@
 
     @ShadeViewRefactor(RefactorComponent.LAYOUT_ALGORITHM)
     protected StackScrollAlgorithm createStackScrollAlgorithm(Context context) {
-        return new StackScrollAlgorithm(context, this);
+        StackScrollAlgorithm stackScrollAlgorithm = new StackScrollAlgorithm(context, this);
+        stackScrollAlgorithm.useRoundnessSourceTypes(mUseRoundnessSourceTypes);
+        return stackScrollAlgorithm;
     }
 
     /**
@@ -3772,7 +3796,7 @@
     }
 
     private void logEmptySpaceClick(MotionEvent ev, boolean isTouchBelowLastNotification,
-            int statusBarState, boolean touchIsClick) {
+                                    int statusBarState, boolean touchIsClick) {
         if (mLogger == null) {
             return;
         }
@@ -3957,7 +3981,9 @@
         mOnEmptySpaceClickListener = listener;
     }
 
-    /** @hide */
+    /**
+     * @hide
+     */
     @Override
     @ShadeViewRefactor(RefactorComponent.INPUT)
     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
@@ -4728,7 +4754,9 @@
         return touchY > mTopPadding + mStackTranslation;
     }
 
-    /** @hide */
+    /**
+     * @hide
+     */
     @Override
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
@@ -5353,7 +5381,9 @@
         return canChildBeCleared(row) && matchesSelection(row, selection);
     }
 
-    /** Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked. */
+    /**
+     * Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked.
+     */
     public void setManageButtonClickListener(@Nullable OnClickListener listener) {
         mManageButtonClickListener = listener;
         if (mFooterView != null) {
@@ -5418,6 +5448,7 @@
     /**
      * Set how far the wake up is when waking up from pulsing. This is a height and will adjust the
      * notification positions accordingly.
+     *
      * @param height the new wake up height
      * @return the overflow how much the height is further than he lowest notification
      */
@@ -5649,7 +5680,7 @@
      * Set rounded rect clipping bounds on this view.
      */
     public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
-            int bottomRadius) {
+                                         int bottomRadius) {
         if (mRoundedRectClippingLeft == left && mRoundedRectClippingRight == right
                 && mRoundedRectClippingBottom == bottom && mRoundedRectClippingTop == top
                 && mBgCornerRadii[0] == topRadius && mBgCornerRadii[5] == bottomRadius) {
@@ -5710,7 +5741,7 @@
         mLaunchingNotification = launching;
         mLaunchingNotificationNeedsToBeClipped = mLaunchAnimationParams != null
                 && (mLaunchAnimationParams.getStartRoundedTopClipping() > 0
-                        || mLaunchAnimationParams.getParentStartRoundedTopClipping() > 0);
+                || mLaunchAnimationParams.getParentStartRoundedTopClipping() > 0);
         if (!mLaunchingNotificationNeedsToBeClipped || !mLaunchingNotification) {
             mLaunchedNotificationClipPath.reset();
         }
@@ -5748,7 +5779,7 @@
                 mLaunchAnimationParams.getProgress(0,
                         NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
         int top = (int) Math.min(MathUtils.lerp(mRoundedRectClippingTop,
-                mLaunchAnimationParams.getTop(), expandProgress),
+                        mLaunchAnimationParams.getTop(), expandProgress),
                 mRoundedRectClippingTop);
         float topRadius = mLaunchAnimationParams.getTopCornerRadius();
         float bottomRadius = mLaunchAnimationParams.getBottomCornerRadius();
@@ -5872,25 +5903,25 @@
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public interface OnOverscrollTopChangedListener {
 
-    /**
-     * Notifies a listener that the overscroll has changed.
-     *
-     * @param amount         the amount of overscroll, in pixels
-     * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
-     *                       unrubberbanded motion to directly expand overscroll view (e.g
-     *                       expand
-     *                       QS)
-     */
-    void onOverscrollTopChanged(float amount, boolean isRubberbanded);
+        /**
+         * Notifies a listener that the overscroll has changed.
+         *
+         * @param amount         the amount of overscroll, in pixels
+         * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
+         *                       unrubberbanded motion to directly expand overscroll view (e.g
+         *                       expand
+         *                       QS)
+         */
+        void onOverscrollTopChanged(float amount, boolean isRubberbanded);
 
-    /**
-     * Notify a listener that the scroller wants to escape from the scrolling motion and
-     * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
-     *
-     * @param velocity The velocity that the Scroller had when over flinging
-     * @param open     Should the fling open or close the overscroll view.
-     */
-    void flingTopOverscroll(float velocity, boolean open);
+        /**
+         * Notify a listener that the scroller wants to escape from the scrolling motion and
+         * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
+         *
+         * @param velocity The velocity that the Scroller had when over flinging
+         * @param open     Should the fling open or close the overscroll view.
+         */
+        void flingTopOverscroll(float velocity, boolean open);
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -6252,7 +6283,9 @@
         }
     };
 
-    public HeadsUpTouchHelper.Callback getHeadsUpCallback() { return mHeadsUpCallback; }
+    public HeadsUpTouchHelper.Callback getHeadsUpCallback() {
+        return mHeadsUpCallback;
+    }
 
     void onGroupExpandChanged(ExpandableNotificationRow changedRow, boolean expanded) {
         boolean animated = mAnimationsEnabled && (mIsExpanded || changedRow.isPinned());
@@ -6357,15 +6390,25 @@
         return mLastSentExpandedHeight;
     }
 
-    /** Enum for selecting some or all notification rows (does not included non-notif views). */
+    /**
+     * Enum for selecting some or all notification rows (does not included non-notif views).
+     */
     @Retention(SOURCE)
     @IntDef({ROWS_ALL, ROWS_HIGH_PRIORITY, ROWS_GENTLE})
-    @interface SelectedRows {}
-    /** All rows representing notifs. */
+    @interface SelectedRows {
+    }
+
+    /**
+     * All rows representing notifs.
+     */
     public static final int ROWS_ALL = 0;
-    /** Only rows where entry.isHighPriority() is true. */
+    /**
+     * Only rows where entry.isHighPriority() is true.
+     */
     public static final int ROWS_HIGH_PRIORITY = 1;
-    /** Only rows where entry.isHighPriority() is false. */
+    /**
+     * Only rows where entry.isHighPriority() is false.
+     */
     public static final int ROWS_GENTLE = 2;
 
     interface ClearAllListener {
@@ -6380,4 +6423,16 @@
         void onAnimationEnd(
                 List<ExpandableNotificationRow> viewsToRemove, @SelectedRows int selectedRows);
     }
+
+    /**
+     *
+     */
+    public interface OnNotificationRemovedListener {
+        /**
+         *
+         * @param child
+         * @param isTransferInProgress
+         */
+        void onNotificationRemoved(ExpandableView child, boolean isTransferInProgress);
+    }
 }
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 e3336b2..4bcc0b6 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
@@ -182,10 +182,12 @@
     private NotificationStackScrollLayout mView;
     private boolean mFadeNotificationsOnDismiss;
     private NotificationSwipeHelper mSwipeHelper;
-    @Nullable private Boolean mHistoryEnabled;
+    @Nullable
+    private Boolean mHistoryEnabled;
     private int mBarState;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
     private final FeatureFlags mFeatureFlags;
+    private final boolean mUseRoundnessSourceTypes;
     private final NotificationTargetsHelper mNotificationTargetsHelper;
 
     private View mLongPressedView;
@@ -391,7 +393,7 @@
                     if (item != null) {
                         Point origin = provider.getRevealAnimationOrigin();
                         mNotificationGutsManager.openGuts(row, origin.x, origin.y, item);
-                    } else  {
+                    } else {
                         Log.e(TAG, "Provider has shouldShowGutsOnSnapOpen, but provided no "
                                 + "menu item in menuItemtoExposeOnSnap. Skipping.");
                     }
@@ -420,7 +422,7 @@
 
                 @Override
                 public void onSnooze(StatusBarNotification sbn,
-                        NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
+                                     NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
                     mCentralSurfaces.setNotificationSnoozed(sbn, snoozeOption);
                 }
 
@@ -544,7 +546,7 @@
 
                 @Override
                 public boolean updateSwipeProgress(View animView, boolean dismissable,
-                        float swipeProgress) {
+                                                   float swipeProgress) {
                     // Returning true prevents alpha fading.
                     return !mFadeNotificationsOnDismiss;
                 }
@@ -584,16 +586,22 @@
 
                 @Override
                 public void onHeadsUpPinned(NotificationEntry entry) {
-                    mNotificationRoundnessManager.updateView(entry.getRow(), false /* animate */);
+                    if (!mUseRoundnessSourceTypes) {
+                        mNotificationRoundnessManager.updateView(
+                                entry.getRow(),
+                                /* animate = */ false);
+                    }
                 }
 
                 @Override
                 public void onHeadsUpUnPinned(NotificationEntry entry) {
-                    ExpandableNotificationRow row = entry.getRow();
-                    // update the roundedness posted, because we might be animating away the
-                    // headsup soon, so no need to set the roundedness to 0 and then back to 1.
-                    row.post(() -> mNotificationRoundnessManager.updateView(row,
-                            true /* animate */));
+                    if (!mUseRoundnessSourceTypes) {
+                        ExpandableNotificationRow row = entry.getRow();
+                        // update the roundedness posted, because we might be animating away the
+                        // headsup soon, so no need to set the roundedness to 0 and then back to 1.
+                        row.post(() -> mNotificationRoundnessManager.updateView(row,
+                                true /* animate */));
+                    }
                 }
 
                 @Override
@@ -603,8 +611,10 @@
                     mView.setNumHeadsUp(numEntries);
                     mView.setTopHeadsUpEntry(topEntry);
                     generateHeadsUpAnimation(entry, isHeadsUp);
-                    ExpandableNotificationRow row = entry.getRow();
-                    mNotificationRoundnessManager.updateView(row, true /* animate */);
+                    if (!mUseRoundnessSourceTypes) {
+                        ExpandableNotificationRow row = entry.getRow();
+                        mNotificationRoundnessManager.updateView(row, true /* animate */);
+                    }
                 }
             };
 
@@ -697,6 +707,7 @@
         mSeenNotificationsProvider = seenNotificationsProvider;
         mShadeController = shadeController;
         mFeatureFlags = featureFlags;
+        mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
         mNotificationTargetsHelper = notificationTargetsHelper;
         updateResources();
     }
@@ -763,8 +774,10 @@
         mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener);
 
         mFadeNotificationsOnDismiss = mFeatureFlags.isEnabled(Flags.NOTIFICATION_DISMISSAL_FADE);
-        mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate);
-        mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded);
+        if (!mUseRoundnessSourceTypes) {
+            mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate);
+            mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded);
+        }
 
         mVisibilityLocationProviderDelegator.setDelegate(this::isInVisibleLocation);
 
@@ -983,7 +996,7 @@
     }
 
     public boolean isAddOrRemoveAnimationPending() {
-        return mView.isAddOrRemoveAnimationPending();
+        return mView != null && mView.isAddOrRemoveAnimationPending();
     }
 
     public int getVisibleNotificationCount() {
@@ -997,9 +1010,11 @@
                 Log.wtf(TAG, "isHistoryEnabled failed to initialize its value");
                 return false;
             }
-            mHistoryEnabled = historyEnabled =
-                    Settings.Secure.getIntForUser(mView.getContext().getContentResolver(),
-                    Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, UserHandle.USER_CURRENT) == 1;
+            mHistoryEnabled = historyEnabled = Settings.Secure.getIntForUser(
+                    mView.getContext().getContentResolver(),
+                    Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
+                    0,
+                    UserHandle.USER_CURRENT) == 1;
         }
         return historyEnabled;
     }
@@ -1029,7 +1044,7 @@
     }
 
     public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
-            boolean cancelAnimators) {
+                                    boolean cancelAnimators) {
         mView.setOverScrollAmount(amount, onTop, animate, cancelAnimators);
     }
 
@@ -1140,7 +1155,9 @@
     }
 
     public void setAlpha(float alpha) {
-        mView.setAlpha(alpha);
+        if (mView != null) {
+            mView.setAlpha(alpha);
+        }
     }
 
     public float calculateAppearFraction(float height) {
@@ -1205,7 +1222,7 @@
 
     /**
      * Update whether we should show the empty shade view ("no notifications" in the shade).
-     *
+     * <p>
      * When in split mode, notifications are always visible regardless of the state of the
      * QuickSettings panel. That being the case, empty view is always shown if the other conditions
      * are true.
@@ -1231,7 +1248,7 @@
 
     /**
      * @return true if {@link StatusBarStateController} is in transition to the KEYGUARD
-     *         and false otherwise.
+     * and false otherwise.
      */
     private boolean isInTransitionToKeyguard() {
         final int currentState = mStatusBarStateController.getState();
@@ -1263,7 +1280,9 @@
         mView.setExpandedHeight(expandedHeight);
     }
 
-    /** Sets the QS header. Used to check if a touch is within its bounds. */
+    /**
+     * Sets the QS header. Used to check if a touch is within its bounds.
+     */
     public void setQsHeader(ViewGroup view) {
         mView.setQsHeader(view);
     }
@@ -1326,7 +1345,7 @@
     public RemoteInputController.Delegate createDelegate() {
         return new RemoteInputController.Delegate() {
             public void setRemoteInputActive(NotificationEntry entry,
-                    boolean remoteInputActive) {
+                                             boolean remoteInputActive) {
                 mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
                 entry.notifyHeightChanged(true /* needsAnimation */);
                 updateFooter();
@@ -1457,7 +1476,7 @@
     }
 
     private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove,
-            @SelectedRows int selectedRows) {
+                                @SelectedRows int selectedRows) {
         if (selectedRows == ROWS_ALL) {
             mNotifCollection.dismissAllNotifications(
                     mLockscreenUserManager.getCurrentUserId());
@@ -1500,8 +1519,8 @@
 
     /**
      * @return the inset during the full shade transition, that needs to be added to the position
-     *         of the quick settings edge. This is relevant for media, that is transitioning
-     *         from the keyguard host to the quick settings one.
+     * of the quick settings edge. This is relevant for media, that is transitioning
+     * from the keyguard host to the quick settings one.
      */
     public int getFullShadeTransitionInset() {
         MediaContainerView view = mKeyguardMediaController.getSinglePaneContainer();
@@ -1515,10 +1534,10 @@
     /**
      * @param fraction The fraction of lockscreen to shade transition.
      *                 0f for all other states.
-     *
-     * Once the lockscreen to shade transition completes and the shade is 100% open,
-     * LockscreenShadeTransitionController resets amount and fraction to 0, where they remain
-     * until the next lockscreen-to-shade transition.
+     *                 <p>
+     *                 Once the lockscreen to shade transition completes and the shade is 100% open,
+     *                 LockscreenShadeTransitionController resets amount and fraction to 0, where
+     *                 they remain until the next lockscreen-to-shade transition.
      */
     public void setTransitionToFullShadeAmount(float fraction) {
         mView.setFractionToShade(fraction);
@@ -1531,7 +1550,9 @@
         mView.setExtraTopInsetForFullShadeTransition(overScrollAmount);
     }
 
-    /** */
+    /**
+     *
+     */
     public void setWillExpand(boolean willExpand) {
         mView.setWillExpand(willExpand);
     }
@@ -1547,7 +1568,7 @@
      * Set rounded rect clipping bounds on this view.
      */
     public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
-            int bottomRadius) {
+                                         int bottomRadius) {
         mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius);
     }
 
@@ -1567,6 +1588,15 @@
     }
 
     /**
+     * Set the remove notification listener
+     * @param listener callback for notification removed
+     */
+    public void setOnNotificationRemovedListener(
+            NotificationStackScrollLayout.OnNotificationRemovedListener listener) {
+        mView.setOnNotificationRemovedListener(listener);
+    }
+
+    /**
      * Enum for UiEvent logged from this class
      */
     enum NotificationPanelEvent implements UiEventLogger.UiEventEnum {
@@ -1576,10 +1606,13 @@
         @UiEvent(doc = "User dismissed all silent notifications from notification panel.")
         DISMISS_SILENT_NOTIFICATIONS_PANEL(314);
         private final int mId;
+
         NotificationPanelEvent(int id) {
             mId = id;
         }
-        @Override public int getId() {
+
+        @Override
+        public int getId() {
             return mId;
         }
 
@@ -1720,8 +1753,12 @@
         @Override
         public void bindRow(ExpandableNotificationRow row) {
             row.setHeadsUpAnimatingAwayListener(animatingAway -> {
-                mNotificationRoundnessManager.updateView(row, false);
-                mHeadsUpAppearanceController.updateHeader(row.getEntry());
+                if (!mUseRoundnessSourceTypes) {
+                    mNotificationRoundnessManager.updateView(row, false);
+                }
+                NotificationEntry entry = row.getEntry();
+                mHeadsUpAppearanceController.updateHeader(entry);
+                mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(entry);
             });
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index ee57411..aaf9300 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -20,6 +20,7 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_SWIPE;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.content.res.Resources;
 import android.graphics.Rect;
@@ -34,9 +35,11 @@
 import com.android.systemui.SwipeHelper;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 
@@ -49,6 +52,7 @@
     @VisibleForTesting
     protected static final long COVER_MENU_DELAY = 4000;
     private static final String TAG = "NotificationSwipeHelper";
+    private static final SourceType SWIPE_DISMISS = SourceType.from("SwipeDismiss");
     private final Runnable mFalsingCheck;
     private View mTranslatingParentView;
     private View mMenuExposedView;
@@ -64,13 +68,21 @@
     private WeakReference<NotificationMenuRowPlugin> mCurrMenuRowRef;
     private boolean mIsExpanded;
     private boolean mPulsing;
+    private final NotificationRoundnessManager mNotificationRoundnessManager;
+    private final boolean mUseRoundnessSourceTypes;
 
     NotificationSwipeHelper(
-            Resources resources, ViewConfiguration viewConfiguration,
-            FalsingManager falsingManager, FeatureFlags featureFlags,
-            int swipeDirection, NotificationCallback callback,
-            NotificationMenuRowPlugin.OnMenuEventListener menuListener) {
+            Resources resources,
+            ViewConfiguration viewConfiguration,
+            FalsingManager falsingManager,
+            FeatureFlags featureFlags,
+            int swipeDirection,
+            NotificationCallback callback,
+            NotificationMenuRowPlugin.OnMenuEventListener menuListener,
+            NotificationRoundnessManager notificationRoundnessManager) {
         super(swipeDirection, callback, resources, viewConfiguration, falsingManager, featureFlags);
+        mNotificationRoundnessManager = notificationRoundnessManager;
+        mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
         mMenuListener = menuListener;
         mCallback = callback;
         mFalsingCheck = () -> resetExposedMenuView(true /* animate */, true /* force */);
@@ -304,6 +316,33 @@
         handleMenuCoveredOrDismissed();
     }
 
+    @Override
+    protected void prepareDismissAnimation(View view, Animator anim) {
+        super.prepareDismissAnimation(view, anim);
+
+        if (mUseRoundnessSourceTypes
+                && view instanceof ExpandableNotificationRow
+                && mNotificationRoundnessManager.isClearAllInProgress()) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    row.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, SWIPE_DISMISS);
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    row.requestRoundnessReset(SWIPE_DISMISS);
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    row.requestRoundnessReset(SWIPE_DISMISS);
+                }
+            });
+        }
+    }
+
     @VisibleForTesting
     protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
         super.dismissChild(view, velocity, useAccelerateInterpolator);
@@ -521,14 +560,17 @@
         private int mSwipeDirection;
         private NotificationCallback mNotificationCallback;
         private NotificationMenuRowPlugin.OnMenuEventListener mOnMenuEventListener;
+        private NotificationRoundnessManager mNotificationRoundnessManager;
 
         @Inject
         Builder(@Main Resources resources, ViewConfiguration viewConfiguration,
-                FalsingManager falsingManager, FeatureFlags featureFlags) {
+                FalsingManager falsingManager, FeatureFlags featureFlags,
+                NotificationRoundnessManager notificationRoundnessManager) {
             mResources = resources;
             mViewConfiguration = viewConfiguration;
             mFalsingManager = falsingManager;
             mFeatureFlags = featureFlags;
+            mNotificationRoundnessManager = notificationRoundnessManager;
         }
 
         Builder setSwipeDirection(int swipeDirection) {
@@ -549,7 +591,8 @@
 
         NotificationSwipeHelper build() {
             return new NotificationSwipeHelper(mResources, mViewConfiguration, mFalsingManager,
-                    mFeatureFlags, mSwipeDirection, mNotificationCallback, mOnMenuEventListener);
+                    mFeatureFlags, mSwipeDirection, mNotificationCallback, mOnMenuEventListener,
+                    mNotificationRoundnessManager);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
index 991a14b..548d1a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
@@ -20,8 +20,7 @@
 constructor(
     featureFlags: FeatureFlags,
 ) {
-    private val isNotificationGroupCornerEnabled =
-        featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)
+    private val useRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES)
 
     /**
      * This method looks for views that can be rounded (and implement [Roundable]) during a
@@ -48,7 +47,7 @@
         if (notificationParent != null && childrenContainer != null) {
             // We are inside a notification group
 
-            if (!isNotificationGroupCornerEnabled) {
+            if (!useRoundnessSourceTypes) {
                 return RoundableTargets(null, null, null)
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index d8c6878..aff7b4c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -31,6 +31,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.LegacySourceType;
 import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -51,6 +52,7 @@
 
     private static final String TAG = "StackScrollAlgorithm";
     private static final Boolean DEBUG = false;
+    private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm");
 
     private final ViewGroup mHostView;
     private float mPaddingBetweenElements;
@@ -70,6 +72,7 @@
     private float mQuickQsOffsetHeight;
     private float mSmallCornerRadius;
     private float mLargeCornerRadius;
+    private boolean mUseRoundnessSourceTypes;
 
     public StackScrollAlgorithm(
             Context context,
@@ -129,7 +132,7 @@
     }
 
     private void updateAlphaState(StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState) {
+                                  AmbientState ambientState) {
         for (ExpandableView view : algorithmState.visibleChildren) {
             final ViewState viewState = view.getViewState();
 
@@ -229,7 +232,7 @@
     }
 
     private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState) {
+                                               AmbientState ambientState) {
         int childCount = algorithmState.visibleChildren.size();
         for (int i = 0; i < childCount; i++) {
             ExpandableView v = algorithmState.visibleChildren.get(i);
@@ -241,7 +244,7 @@
     }
 
     private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState,
-            int speedBumpIndex) {
+                                      int speedBumpIndex) {
         int childCount = algorithmState.visibleChildren.size();
         int belowSpeedBump = speedBumpIndex;
         for (int i = 0; i < childCount; i++) {
@@ -268,7 +271,7 @@
     }
 
     private void updateClipping(StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState) {
+                                AmbientState ambientState) {
         float drawStart = ambientState.isOnKeyguard() ? 0
                 : ambientState.getStackY() - ambientState.getScrollY();
         float clipStart = 0;
@@ -314,7 +317,7 @@
      * Updates the dimmed, activated and hiding sensitive states of the children.
      */
     private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
-            StackScrollAlgorithmState algorithmState) {
+                                                    StackScrollAlgorithmState algorithmState) {
         boolean dimmed = ambientState.isDimmed();
         boolean hideSensitive = ambientState.isHideSensitive();
         View activatedChild = ambientState.getActivatedChild();
@@ -408,7 +411,7 @@
     }
 
     private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
-            ExpandableView v) {
+                                   ExpandableView v) {
         ExpandableViewState viewState = v.getViewState();
         viewState.notGoneIndex = notGoneIndex;
         state.visibleChildren.add(v);
@@ -434,7 +437,7 @@
      * @param ambientState   The current ambient state
      */
     protected void updatePositionsForState(StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState) {
+                                           AmbientState ambientState) {
         if (!ambientState.isOnKeyguard()
                 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
             algorithmState.mCurrentYPosition += mNotificationScrimPadding;
@@ -448,7 +451,7 @@
     }
 
     private void setLocation(ExpandableViewState expandableViewState, float currentYPosition,
-            int i) {
+                             int i) {
         expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
         if (currentYPosition <= 0) {
             expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
@@ -496,9 +499,13 @@
     }
 
     @VisibleForTesting
-    void maybeUpdateHeadsUpIsVisible(ExpandableViewState viewState, boolean isShadeExpanded,
-            boolean mustStayOnScreen, boolean topVisible, float viewEnd, float hunMax) {
-
+    void maybeUpdateHeadsUpIsVisible(
+            ExpandableViewState viewState,
+            boolean isShadeExpanded,
+            boolean mustStayOnScreen,
+            boolean topVisible,
+            float viewEnd,
+            float hunMax) {
         if (isShadeExpanded && mustStayOnScreen && topVisible) {
             viewState.headsUpIsVisible = viewEnd < hunMax;
         }
@@ -676,7 +683,7 @@
     }
 
     private void updatePulsingStates(StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState) {
+                                     AmbientState ambientState) {
         int childCount = algorithmState.visibleChildren.size();
         for (int i = 0; i < childCount; i++) {
             View child = algorithmState.visibleChildren.get(i);
@@ -693,7 +700,7 @@
     }
 
     private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState) {
+                                     AmbientState ambientState) {
         int childCount = algorithmState.visibleChildren.size();
 
         // Move the tracked heads up into position during the appear animation, by interpolating
@@ -777,7 +784,7 @@
      */
     @VisibleForTesting
     void clampHunToTop(float quickQsOffsetHeight, float stackTranslation, float collapsedHeight,
-            ExpandableViewState viewState) {
+                       ExpandableViewState viewState) {
 
         final float newTranslation = Math.max(quickQsOffsetHeight + stackTranslation,
                 viewState.getYTranslation());
@@ -792,7 +799,7 @@
     // Pin HUN to bottom of expanded QS
     // while the rest of notifications are scrolled offscreen.
     private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
-            ExpandableViewState childState) {
+                                          ExpandableViewState childState) {
         float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
         final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
                 + ambientState.getStackTranslation();
@@ -807,14 +814,19 @@
         // Animate pinned HUN bottom corners to and from original roundness.
         final float originalCornerRadius =
                 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
-        final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
+        final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
                 ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
-        row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll);
+        if (mUseRoundnessSourceTypes) {
+            row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO);
+            row.addOnDetachResetRoundness(STACK_SCROLL_ALGO);
+        } else {
+            row.requestBottomRoundness(bottomValue, LegacySourceType.OnScroll);
+        }
     }
 
     @VisibleForTesting
     float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY,
-            float viewMaxHeight, float originalCornerRadius) {
+                                             float viewMaxHeight, float originalCornerRadius) {
 
         // Compute y where corner roundness should be in its original unpinned state.
         // We use view max height because the pinned collapsed HUN expands to max height
@@ -844,7 +856,7 @@
      * @param ambientState   The ambient state of the algorithm
      */
     private void updateZValuesForState(StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState) {
+                                       AmbientState ambientState) {
         int childCount = algorithmState.visibleChildren.size();
         float childrenOnTop = 0.0f;
 
@@ -876,9 +888,9 @@
      *                      previous HUNs whose Z positions are greater than 0.
      */
     protected float updateChildZValue(int i, float childrenOnTop,
-            StackScrollAlgorithmState algorithmState,
-            AmbientState ambientState,
-            boolean isTopHun) {
+                                      StackScrollAlgorithmState algorithmState,
+                                      AmbientState ambientState,
+                                      boolean isTopHun) {
         ExpandableView child = algorithmState.visibleChildren.get(i);
         ExpandableViewState childViewState = child.getViewState();
         float baseZ = ambientState.getBaseZHeight();
@@ -950,6 +962,14 @@
         this.mIsExpanded = isExpanded;
     }
 
+    /**
+     * Enable the support for rounded corner based on the SourceType
+     * @param enabled true if is supported
+     */
+    public void useRoundnessSourceTypes(boolean enabled) {
+        mUseRoundnessSourceTypes = enabled;
+    }
+
     public static class StackScrollAlgorithmState {
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 484441a..c217ab3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -19,11 +19,14 @@
 import static com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentModule.OPERATOR_NAME_FRAME_VIEW;
 
 import android.graphics.Rect;
+import android.util.MathUtils;
 import android.view.View;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.widget.ViewClippingUtil;
 import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
@@ -32,8 +35,10 @@
 import com.android.systemui.statusbar.HeadsUpStatusBarView;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope;
 import com.android.systemui.statusbar.policy.Clock;
@@ -51,6 +56,7 @@
 
 /**
  * Controls the appearance of heads up notifications in the icon area and the header itself.
+ * It also controls the roundness of the heads up notifications and the pulsing notifications.
  */
 @StatusBarFragmentScope
 public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBarView>
@@ -59,12 +65,17 @@
         NotificationWakeUpCoordinator.WakeUpListener {
     public static final int CONTENT_FADE_DURATION = 110;
     public static final int CONTENT_FADE_DELAY = 100;
+
+    private static final SourceType HEADS_UP = SourceType.from("HeadsUp");
+    private static final SourceType PULSING = SourceType.from("Pulsing");
     private final NotificationIconAreaController mNotificationIconAreaController;
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final NotificationStackScrollLayoutController mStackScrollerController;
 
     private final DarkIconDispatcher mDarkIconDispatcher;
     private final NotificationPanelViewController mNotificationPanelViewController;
+    private final NotificationRoundnessManager mNotificationRoundnessManager;
+    private final boolean mUseRoundnessSourceTypes;
     private final Consumer<ExpandableNotificationRow>
             mSetTrackingHeadsUp = this::setTrackingHeadsUp;
     private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction;
@@ -105,11 +116,15 @@
             CommandQueue commandQueue,
             NotificationStackScrollLayoutController stackScrollerController,
             NotificationPanelViewController notificationPanelViewController,
+            NotificationRoundnessManager notificationRoundnessManager,
+            FeatureFlags featureFlags,
             HeadsUpStatusBarView headsUpStatusBarView,
             Clock clockView,
             @Named(OPERATOR_NAME_FRAME_VIEW) Optional<View> operatorNameViewOptional) {
         super(headsUpStatusBarView);
         mNotificationIconAreaController = notificationIconAreaController;
+        mNotificationRoundnessManager = notificationRoundnessManager;
+        mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
         mHeadsUpManager = headsUpManager;
 
         // We may be mid-HUN-expansion when this controller is re-created (for example, if the user
@@ -179,6 +194,7 @@
     public void onHeadsUpPinned(NotificationEntry entry) {
         updateTopEntry();
         updateHeader(entry);
+        updateHeadsUpAndPulsingRoundness(entry);
     }
 
     private void updateTopEntry() {
@@ -316,6 +332,7 @@
     public void onHeadsUpUnPinned(NotificationEntry entry) {
         updateTopEntry();
         updateHeader(entry);
+        updateHeadsUpAndPulsingRoundness(entry);
     }
 
     public void setAppearFraction(float expandedHeight, float appearFraction) {
@@ -346,7 +363,9 @@
         ExpandableNotificationRow previousTracked = mTrackedChild;
         mTrackedChild = trackedChild;
         if (previousTracked != null) {
-            updateHeader(previousTracked.getEntry());
+            NotificationEntry entry = previousTracked.getEntry();
+            updateHeader(entry);
+            updateHeadsUpAndPulsingRoundness(entry);
         }
     }
 
@@ -357,6 +376,7 @@
     private void updateHeadsUpHeaders() {
         mHeadsUpManager.getAllEntries().forEach(entry -> {
             updateHeader(entry);
+            updateHeadsUpAndPulsingRoundness(entry);
         });
     }
 
@@ -370,6 +390,31 @@
         row.setHeaderVisibleAmount(headerVisibleAmount);
     }
 
+    /**
+     * Update the HeadsUp and the Pulsing roundness based on current state
+     * @param entry target notification
+     */
+    public void updateHeadsUpAndPulsingRoundness(NotificationEntry entry) {
+        if (mUseRoundnessSourceTypes) {
+            ExpandableNotificationRow row = entry.getRow();
+            boolean isTrackedChild = row == mTrackedChild;
+            if (row.isPinned() || row.isHeadsUpAnimatingAway() || isTrackedChild) {
+                float roundness = MathUtils.saturate(1f - mAppearFraction);
+                row.requestRoundness(roundness, roundness, HEADS_UP);
+            } else {
+                row.requestRoundnessReset(HEADS_UP);
+            }
+            if (mNotificationRoundnessManager.shouldRoundNotificationPulsing()) {
+                if (row.showingPulsing()) {
+                    row.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, PULSING);
+                } else {
+                    row.requestRoundnessReset(PULSING);
+                }
+            }
+        }
+    }
+
+
     @Override
     public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) {
         mView.onDarkChanged(areas, darkIntensity, tint);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
index 4969a1c..6811bf6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileController.java
@@ -14,6 +14,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import androidx.annotation.MainThread;
+
 import com.android.systemui.statusbar.phone.ManagedProfileController.Callback;
 import com.android.systemui.statusbar.policy.CallbackController;
 
@@ -25,8 +27,20 @@
 
     boolean isWorkModeEnabled();
 
-    public interface Callback {
+    /**
+     * Callback to get updates about work profile status.
+     */
+    interface Callback {
+        /**
+         * Called when managed profile change is detected. This always runs on the main thread.
+         */
+        @MainThread
         void onManagedProfileChanged();
+
+        /**
+         * Called when managed profile removal is detected. This always runs on the main thread.
+         */
+        @MainThread
         void onManagedProfileRemoved();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
index 4beb87d..abdf827 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -33,31 +33,28 @@
 
 import javax.inject.Inject;
 
-/**
- */
 @SysUISingleton
 public class ManagedProfileControllerImpl implements ManagedProfileController {
 
     private final List<Callback> mCallbacks = new ArrayList<>();
-
+    private final UserTrackerCallback mUserTrackerCallback = new UserTrackerCallback();
     private final Context mContext;
     private final Executor mMainExecutor;
     private final UserManager mUserManager;
     private final UserTracker mUserTracker;
     private final LinkedList<UserInfo> mProfiles;
+
     private boolean mListening;
     private int mCurrentUser;
 
-    /**
-     */
     @Inject
     public ManagedProfileControllerImpl(Context context, @Main Executor mainExecutor,
-            UserTracker userTracker) {
+            UserTracker userTracker, UserManager userManager) {
         mContext = context;
         mMainExecutor = mainExecutor;
-        mUserManager = UserManager.get(mContext);
+        mUserManager = userManager;
         mUserTracker = userTracker;
-        mProfiles = new LinkedList<UserInfo>();
+        mProfiles = new LinkedList<>();
     }
 
     @Override
@@ -100,16 +97,22 @@
                 }
             }
             if (mProfiles.size() == 0 && hadProfile && (user == mCurrentUser)) {
-                for (Callback callback : mCallbacks) {
-                    callback.onManagedProfileRemoved();
-                }
+                mMainExecutor.execute(this::notifyManagedProfileRemoved);
             }
             mCurrentUser = user;
         }
     }
 
+    private void notifyManagedProfileRemoved() {
+        for (Callback callback : mCallbacks) {
+            callback.onManagedProfileRemoved();
+        }
+    }
+
     public boolean hasActiveProfile() {
-        if (!mListening) reloadManagedProfiles();
+        if (!mListening || mUserTracker.getUserId() != mCurrentUser) {
+            reloadManagedProfiles();
+        }
         synchronized (mProfiles) {
             return mProfiles.size() > 0;
         }
@@ -134,28 +137,28 @@
         mListening = listening;
         if (listening) {
             reloadManagedProfiles();
-            mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
+            mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
         } else {
-            mUserTracker.removeCallback(mUserChangedCallback);
+            mUserTracker.removeCallback(mUserTrackerCallback);
         }
     }
 
-    private final UserTracker.Callback mUserChangedCallback =
-            new UserTracker.Callback() {
-                @Override
-                public void onUserChanged(int newUser, @NonNull Context userContext) {
-                    reloadManagedProfiles();
-                    for (Callback callback : mCallbacks) {
-                        callback.onManagedProfileChanged();
-                    }
-                }
+    private final class UserTrackerCallback implements UserTracker.Callback {
 
-                @Override
-                public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
-                    reloadManagedProfiles();
-                    for (Callback callback : mCallbacks) {
-                        callback.onManagedProfileChanged();
-                    }
-                }
-            };
+        @Override
+        public void onUserChanged(int newUser, @NonNull Context userContext) {
+            reloadManagedProfiles();
+            for (Callback callback : mCallbacks) {
+                callback.onManagedProfileChanged();
+            }
+        }
+
+        @Override
+        public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
+            reloadManagedProfiles();
+            for (Callback callback : mCallbacks) {
+                callback.onManagedProfileChanged();
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index fb67f1a..c350c78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
+import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
@@ -24,11 +25,12 @@
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
@@ -40,6 +42,8 @@
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 
 @Module
 abstract class StatusBarPipelineModule {
@@ -52,26 +56,28 @@
     @Binds
     abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
 
-    @Binds
-    abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
+    @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
 
     @Binds
     abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor
 
     @Binds
     abstract fun mobileConnectionsRepository(
-        impl: MobileConnectionsRepositoryImpl
+        impl: MobileRepositorySwitcher
     ): MobileConnectionsRepository
 
-    @Binds
-    abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
+    @Binds abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
 
-    @Binds
-    abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy
+    @Binds abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy
 
     @Binds
     abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor
 
+    @Binds
+    @IntoMap
+    @ClassKey(MobileUiAdapter::class)
+    abstract fun bindFeature(impl: MobileUiAdapter): CoreStartable
+
     @Module
     companion object {
         @JvmStatic
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
index da87f73..5479b92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
@@ -20,6 +20,7 @@
 import android.telephony.TelephonyManager.DATA_CONNECTING
 import android.telephony.TelephonyManager.DATA_DISCONNECTED
 import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_UNKNOWN
 import android.telephony.TelephonyManager.DataState
 
 /** Internal enum representation of the telephony data connection states */
@@ -28,6 +29,7 @@
     Connecting(DATA_CONNECTING),
     Disconnected(DATA_DISCONNECTED),
     Disconnecting(DATA_DISCONNECTING),
+    Unknown(DATA_UNKNOWN),
 }
 
 fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState =
@@ -36,5 +38,6 @@
         DATA_CONNECTING -> DataConnectionState.Connecting
         DATA_DISCONNECTED -> DataConnectionState.Disconnected
         DATA_DISCONNECTING -> DataConnectionState.Disconnecting
-        else -> throw IllegalArgumentException("unknown data state received")
+        DATA_UNKNOWN -> DataConnectionState.Unknown
+        else -> throw IllegalArgumentException("unknown data state received $this")
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 581842b..f094563 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -16,44 +16,13 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
-import android.content.Context
-import android.database.ContentObserver
-import android.provider.Settings.Global
-import android.telephony.CellSignalStrength
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
 import android.telephony.TelephonyCallback
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
 import android.telephony.TelephonyManager
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
-import com.android.systemui.util.settings.GlobalSettings
-import java.lang.IllegalStateException
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
 
 /**
  * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
@@ -80,183 +49,3 @@
      */
     val isDefaultDataSubscription: StateFlow<Boolean>
 }
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-class MobileConnectionRepositoryImpl(
-    private val context: Context,
-    private val subId: Int,
-    private val telephonyManager: TelephonyManager,
-    private val globalSettings: GlobalSettings,
-    defaultDataSubId: StateFlow<Int>,
-    globalMobileDataSettingChangedEvent: Flow<Unit>,
-    bgDispatcher: CoroutineDispatcher,
-    logger: ConnectivityPipelineLogger,
-    scope: CoroutineScope,
-) : MobileConnectionRepository {
-    init {
-        if (telephonyManager.subscriptionId != subId) {
-            throw IllegalStateException(
-                "TelephonyManager should be created with subId($subId). " +
-                    "Found ${telephonyManager.subscriptionId} instead."
-            )
-        }
-    }
-
-    private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
-
-    override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
-        var state = MobileSubscriptionModel()
-        conflatedCallbackFlow {
-                // TODO (b/240569788): log all of these into the connectivity logger
-                val callback =
-                    object :
-                        TelephonyCallback(),
-                        TelephonyCallback.ServiceStateListener,
-                        TelephonyCallback.SignalStrengthsListener,
-                        TelephonyCallback.DataConnectionStateListener,
-                        TelephonyCallback.DataActivityListener,
-                        TelephonyCallback.CarrierNetworkListener,
-                        TelephonyCallback.DisplayInfoListener {
-                        override fun onServiceStateChanged(serviceState: ServiceState) {
-                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
-                            trySend(state)
-                        }
-
-                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
-                            val cdmaLevel =
-                                signalStrength
-                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
-                                    .let { strengths ->
-                                        if (!strengths.isEmpty()) {
-                                            strengths[0].level
-                                        } else {
-                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
-                                        }
-                                    }
-
-                            val primaryLevel = signalStrength.level
-
-                            state =
-                                state.copy(
-                                    cdmaLevel = cdmaLevel,
-                                    primaryLevel = primaryLevel,
-                                    isGsm = signalStrength.isGsm,
-                                )
-                            trySend(state)
-                        }
-
-                        override fun onDataConnectionStateChanged(
-                            dataState: Int,
-                            networkType: Int
-                        ) {
-                            state =
-                                state.copy(dataConnectionState = dataState.toDataConnectionType())
-                            trySend(state)
-                        }
-
-                        override fun onDataActivity(direction: Int) {
-                            state = state.copy(dataActivityDirection = direction)
-                            trySend(state)
-                        }
-
-                        override fun onCarrierNetworkChange(active: Boolean) {
-                            state = state.copy(carrierNetworkChangeActive = active)
-                            trySend(state)
-                        }
-
-                        override fun onDisplayInfoChanged(
-                            telephonyDisplayInfo: TelephonyDisplayInfo
-                        ) {
-                            val networkType =
-                                if (
-                                    telephonyDisplayInfo.overrideNetworkType ==
-                                        OVERRIDE_NETWORK_TYPE_NONE
-                                ) {
-                                    DefaultNetworkType(telephonyDisplayInfo.networkType)
-                                } else {
-                                    OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
-                                }
-                            state = state.copy(resolvedNetworkType = networkType)
-                            trySend(state)
-                        }
-                    }
-                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
-                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
-            }
-            .onEach { telephonyCallbackEvent.tryEmit(Unit) }
-            .logOutputChange(logger, "MobileSubscriptionModel")
-            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
-    }
-
-    /** Produces whenever the mobile data setting changes for this subId */
-    private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
-        val observer =
-            object : ContentObserver(null) {
-                override fun onChange(selfChange: Boolean) {
-                    trySend(Unit)
-                }
-            }
-
-        globalSettings.registerContentObserver(
-            globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
-            /* notifyForDescendants */ true,
-            observer
-        )
-
-        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
-    }
-
-    /**
-     * There are a few cases where we will need to poll [TelephonyManager] so we can update some
-     * internal state where callbacks aren't provided. Any of those events should be merged into
-     * this flow, which can be used to trigger the polling.
-     */
-    private val telephonyPollingEvent: Flow<Unit> =
-        merge(
-            telephonyCallbackEvent,
-            localMobileDataSettingChangedEvent,
-            globalMobileDataSettingChangedEvent,
-        )
-
-    override val dataEnabled: StateFlow<Boolean> =
-        telephonyPollingEvent
-            .mapLatest { dataConnectionAllowed() }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
-
-    private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
-
-    override val isDefaultDataSubscription: StateFlow<Boolean> =
-        defaultDataSubId
-            .mapLatest { it == subId }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
-
-    class Factory
-    @Inject
-    constructor(
-        private val context: Context,
-        private val telephonyManager: TelephonyManager,
-        private val logger: ConnectivityPipelineLogger,
-        private val globalSettings: GlobalSettings,
-        @Background private val bgDispatcher: CoroutineDispatcher,
-        @Application private val scope: CoroutineScope,
-    ) {
-        fun build(
-            subId: Int,
-            defaultDataSubId: StateFlow<Int>,
-            globalMobileDataSettingChangedEvent: Flow<Unit>,
-        ): MobileConnectionRepository {
-            return MobileConnectionRepositoryImpl(
-                context,
-                subId,
-                telephonyManager.createForSubscriptionId(subId),
-                globalSettings,
-                defaultDataSubId,
-                globalMobileDataSettingChangedEvent,
-                bgDispatcher,
-                logger,
-                scope,
-            )
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index c3c1f14..14200f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -16,53 +16,14 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.IntentFilter
-import android.database.ContentObserver
-import android.net.ConnectivityManager
-import android.net.ConnectivityManager.NetworkCallback
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
-import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.provider.Settings
-import android.provider.Settings.Global.MOBILE_DATA
-import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
-import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyManager
-import androidx.annotation.VisibleForTesting
-import com.android.internal.telephony.PhoneConstants
 import com.android.settingslib.mobile.MobileMappings
 import com.android.settingslib.mobile.MobileMappings.Config
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.util.settings.GlobalSettings
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
 
 /**
  * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
@@ -90,202 +51,3 @@
     /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
     val globalMobileDataSettingChangedEvent: Flow<Unit>
 }
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MobileConnectionsRepositoryImpl
-@Inject
-constructor(
-    private val connectivityManager: ConnectivityManager,
-    private val subscriptionManager: SubscriptionManager,
-    private val telephonyManager: TelephonyManager,
-    private val logger: ConnectivityPipelineLogger,
-    broadcastDispatcher: BroadcastDispatcher,
-    private val globalSettings: GlobalSettings,
-    private val context: Context,
-    @Background private val bgDispatcher: CoroutineDispatcher,
-    @Application private val scope: CoroutineScope,
-    private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
-) : MobileConnectionsRepository {
-    private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
-
-    /**
-     * State flow that emits the set of mobile data subscriptions, each represented by its own
-     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
-     * info object, but for now we keep track of the infos themselves.
-     */
-    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
-                        override fun onSubscriptionsChanged() {
-                            trySend(Unit)
-                        }
-                    }
-
-                subscriptionManager.addOnSubscriptionsChangedListener(
-                    bgDispatcher.asExecutor(),
-                    callback,
-                )
-
-                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
-            }
-            .mapLatest { fetchSubscriptionsList() }
-            .onEach { infos -> dropUnusedReposFromCache(infos) }
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
-
-    /** StateFlow that keeps track of the current active mobile data subscription */
-    override val activeMobileDataSubscriptionId: StateFlow<Int> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
-                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
-                            trySend(subId)
-                        }
-                    }
-
-                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
-                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
-            }
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
-
-    private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
-        MutableSharedFlow(extraBufferCapacity = 1)
-
-    override val defaultDataSubId: StateFlow<Int> =
-        broadcastDispatcher
-            .broadcastFlow(
-                IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
-            ) { intent, _ ->
-                intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
-            }
-            .distinctUntilChanged()
-            .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
-            .stateIn(
-                scope,
-                SharingStarted.WhileSubscribed(),
-                SubscriptionManager.getDefaultDataSubscriptionId()
-            )
-
-    private val carrierConfigChangedEvent =
-        broadcastDispatcher.broadcastFlow(
-            IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
-        )
-
-    /**
-     * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
-     * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
-     * config, so this will apply to every icon that we care about.
-     *
-     * Relevant bits in the config are things like
-     * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
-     *
-     * This flow will produce whenever the default data subscription or the carrier config changes.
-     */
-    override val defaultDataSubRatConfig: StateFlow<Config> =
-        merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
-            .mapLatest { Config.readConfig(context) }
-            .stateIn(
-                scope,
-                SharingStarted.WhileSubscribed(),
-                initialValue = Config.readConfig(context)
-            )
-
-    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
-        if (!isValidSubId(subId)) {
-            throw IllegalArgumentException(
-                "subscriptionId $subId is not in the list of valid subscriptions"
-            )
-        }
-
-        return subIdRepositoryCache[subId]
-            ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
-    }
-
-    /**
-     * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
-     * connection repositories also observe the URI for [MOBILE_DATA] + subId.
-     */
-    override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
-        val observer =
-            object : ContentObserver(null) {
-                override fun onChange(selfChange: Boolean) {
-                    trySend(Unit)
-                }
-            }
-
-        globalSettings.registerContentObserver(
-            globalSettings.getUriFor(MOBILE_DATA),
-            true,
-            observer
-        )
-
-        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
-    }
-
-    @SuppressLint("MissingPermission")
-    override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
-                        override fun onLost(network: Network) {
-                            // Send a disconnected model when lost. Maybe should create a sealed
-                            // type or null here?
-                            trySend(MobileConnectivityModel())
-                        }
-
-                        override fun onCapabilitiesChanged(
-                            network: Network,
-                            caps: NetworkCapabilities
-                        ) {
-                            trySend(
-                                MobileConnectivityModel(
-                                    isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
-                                    isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
-                                )
-                            )
-                        }
-                    }
-
-                connectivityManager.registerDefaultNetworkCallback(callback)
-
-                awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
-            }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
-
-    private fun isValidSubId(subId: Int): Boolean {
-        subscriptionsFlow.value.forEach {
-            if (it.subscriptionId == subId) {
-                return true
-            }
-        }
-
-        return false
-    }
-
-    @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
-
-    private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
-        return mobileConnectionRepositoryFactory.build(
-            subId,
-            defaultDataSubId,
-            globalMobileDataSettingChangedEvent,
-        )
-    }
-
-    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
-        // Remove any connection repository from the cache that isn't in the new set of IDs. They
-        // will get garbage collected once their subscribers go away
-        val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
-
-        subIdRepositoryCache.keys.forEach {
-            if (!currentValidSubscriptionIds.contains(it)) {
-                subIdRepositoryCache.remove(it)
-            }
-        }
-    }
-
-    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
-        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
new file mode 100644
index 0000000..e214005
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.os.Bundle
+import android.telephony.SubscriptionInfo
+import androidx.annotation.VisibleForTesting
+import com.android.settingslib.mobile.MobileMappings
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A provider for the [MobileConnectionsRepository] interface that can choose between the Demo and
+ * Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo], which
+ * switches based on the latest information from [DemoModeController], and switches every flow in
+ * the interface to point to the currently-active provider. This allows us to put the demo mode
+ * interface in its own repository, completely separate from the real version, while still using all
+ * of the prod implementations for the rest of the pipeline (interactors and onward). Looks
+ * something like this:
+ *
+ * ```
+ * RealRepository
+ *                 │
+ *                 ├──►RepositorySwitcher──►RealInteractor──►RealViewModel
+ *                 │
+ * DemoRepository
+ * ```
+ *
+ * NOTE: because the UI layer for mobile icons relies on a nested-repository structure, it is likely
+ * that we will have to drain the subscription list whenever demo mode changes. Otherwise if a real
+ * subscription list [1] is replaced with a demo subscription list [1], the view models will not see
+ * a change (due to `distinctUntilChanged`) and will not refresh their data providers to the demo
+ * implementation.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class MobileRepositorySwitcher
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    val realRepository: MobileConnectionsRepositoryImpl,
+    val demoMobileConnectionsRepository: DemoMobileConnectionsRepository,
+    demoModeController: DemoModeController,
+) : MobileConnectionsRepository {
+
+    val isDemoMode: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : DemoMode {
+                        override fun dispatchDemoCommand(command: String?, args: Bundle?) {
+                            // Nothing, we just care about on/off
+                        }
+
+                        override fun onDemoModeStarted() {
+                            demoMobileConnectionsRepository.startProcessingCommands()
+                            trySend(true)
+                        }
+
+                        override fun onDemoModeFinished() {
+                            demoMobileConnectionsRepository.stopProcessingCommands()
+                            trySend(false)
+                        }
+                    }
+
+                demoModeController.addCallback(callback)
+                awaitClose { demoModeController.removeCallback(callback) }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
+
+    // Convenient definition flow for the currently active repo (based on demo mode or not)
+    @VisibleForTesting
+    internal val activeRepo: StateFlow<MobileConnectionsRepository> =
+        isDemoMode
+            .mapLatest { demoMode ->
+                if (demoMode) {
+                    demoMobileConnectionsRepository
+                } else {
+                    realRepository
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository)
+
+    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
+        activeRepo
+            .flatMapLatest { it.subscriptionsFlow }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                realRepository.subscriptionsFlow.value
+            )
+
+    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+        activeRepo
+            .flatMapLatest { it.activeMobileDataSubscriptionId }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                realRepository.activeMobileDataSubscriptionId.value
+            )
+
+    override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
+        activeRepo
+            .flatMapLatest { it.defaultDataSubRatConfig }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                realRepository.defaultDataSubRatConfig.value
+            )
+
+    override val defaultDataSubId: StateFlow<Int> =
+        activeRepo
+            .flatMapLatest { it.defaultDataSubId }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
+
+    override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
+        activeRepo
+            .flatMapLatest { it.defaultMobileNetworkConnectivity }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                realRepository.defaultMobileNetworkConnectivity.value
+            )
+
+    override val globalMobileDataSettingChangedEvent: Flow<Unit> =
+        activeRepo.flatMapLatest { it.globalMobileDataSettingChangedEvent }
+
+    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+        if (isDemoMode.value) {
+            return demoMobileConnectionsRepository.getRepoForSubId(subId)
+        }
+        return realRepository.getRepoForSubId(subId)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
new file mode 100644
index 0000000..5f2feb2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
+
+import android.content.Context
+import android.telephony.Annotation
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED
+import android.telephony.TelephonyManager.NETWORK_TYPE_GSM
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_NR
+import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import android.util.Log
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** This repository vends out data based on demo mode commands */
+@OptIn(ExperimentalCoroutinesApi::class)
+class DemoMobileConnectionsRepository
+@Inject
+constructor(
+    private val dataSource: DemoModeMobileConnectionDataSource,
+    @Application private val scope: CoroutineScope,
+    context: Context,
+) : MobileConnectionsRepository {
+
+    private var demoCommandJob: Job? = null
+
+    private val connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>()
+    private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionInfo>()
+    val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+
+    private val _subscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
+    override val subscriptionsFlow =
+        _subscriptions
+            .onEach { infos -> dropUnusedReposFromCache(infos) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), _subscriptions.value)
+
+    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
+        // Remove any connection repository from the cache that isn't in the new set of IDs. They
+        // will get garbage collected once their subscribers go away
+        val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
+
+        connectionRepoCache.keys.forEach {
+            if (!currentValidSubscriptionIds.contains(it)) {
+                connectionRepoCache.remove(it)
+            }
+        }
+    }
+
+    private fun maybeCreateSubscription(subId: Int) {
+        if (!subscriptionInfoCache.containsKey(subId)) {
+            createSubscriptionForSubId(subId, subId).also { subscriptionInfoCache[subId] = it }
+
+            _subscriptions.value = subscriptionInfoCache.values.toList()
+        }
+    }
+
+    /** Mimics the old NetworkControllerImpl for now */
+    private fun createSubscriptionForSubId(subId: Int, slotIndex: Int): SubscriptionInfo {
+        return SubscriptionInfo(
+            subId,
+            "",
+            slotIndex,
+            "",
+            "",
+            0,
+            0,
+            "",
+            0,
+            null,
+            null,
+            null,
+            "",
+            false,
+            null,
+            null,
+        )
+    }
+
+    // TODO(b/261029387): add a command for this value
+    override val activeMobileDataSubscriptionId =
+        subscriptionsFlow
+            .mapLatest { infos ->
+                // For now, active is just the first in the list
+                infos.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
+            }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                subscriptionsFlow.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
+            )
+
+    /** Demo mode doesn't currently support modifications to the mobile mappings */
+    override val defaultDataSubRatConfig =
+        MutableStateFlow(MobileMappings.Config.readConfig(context))
+
+    // TODO(b/261029387): add a command for this value
+    override val defaultDataSubId =
+        activeMobileDataSubscriptionId.stateIn(
+            scope,
+            SharingStarted.WhileSubscribed(),
+            INVALID_SUBSCRIPTION_ID
+        )
+
+    // TODO(b/261029387): not yet supported
+    override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
+
+    override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
+        return connectionRepoCache[subId]
+            ?: DemoMobileConnectionRepository(subId).also { connectionRepoCache[subId] = it }
+    }
+
+    override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
+
+    fun startProcessingCommands() {
+        demoCommandJob =
+            scope.launch {
+                dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) }
+            }
+    }
+
+    fun stopProcessingCommands() {
+        demoCommandJob?.cancel()
+        _subscriptions.value = listOf()
+        connectionRepoCache.clear()
+        subscriptionInfoCache.clear()
+    }
+
+    private fun processEvent(event: FakeNetworkEventModel) {
+        when (event) {
+            is Mobile -> {
+                processEnabledMobileState(event)
+            }
+            is MobileDisabled -> {
+                processDisabledMobileState(event)
+            }
+        }
+    }
+
+    private fun processEnabledMobileState(state: Mobile) {
+        // get or create the connection repo, and set its values
+        val subId = state.subId ?: DEFAULT_SUB_ID
+        maybeCreateSubscription(subId)
+
+        val connection = getRepoForSubId(subId)
+        // This is always true here, because we split out disabled states at the data-source level
+        connection.dataEnabled.value = true
+        connection.isDefaultDataSubscription.value = state.dataType != null
+
+        connection.subscriptionModelFlow.value = state.toMobileSubscriptionModel()
+    }
+
+    private fun processDisabledMobileState(state: MobileDisabled) {
+        if (_subscriptions.value.isEmpty()) {
+            // Nothing to do here
+            return
+        }
+
+        val subId =
+            state.subId
+                ?: run {
+                    // For sake of usability, we can allow for no subId arg if there is only one
+                    // subscription
+                    if (_subscriptions.value.size > 1) {
+                        Log.d(
+                            TAG,
+                            "processDisabledMobileState: Unable to infer subscription to " +
+                                "disable. Specify subId using '-e slot <subId>'" +
+                                "Known subIds: [${subIdsString()}]"
+                        )
+                        return
+                    }
+
+                    // Use the only existing subscription as our arg, since there is only one
+                    _subscriptions.value[0].subscriptionId
+                }
+
+        removeSubscription(subId)
+    }
+
+    private fun removeSubscription(subId: Int) {
+        val currentSubscriptions = _subscriptions.value
+        subscriptionInfoCache.remove(subId)
+        _subscriptions.value = currentSubscriptions.filter { it.subscriptionId != subId }
+    }
+
+    private fun subIdsString(): String =
+        _subscriptions.value.joinToString(",") { it.subscriptionId.toString() }
+
+    companion object {
+        private const val TAG = "DemoMobileConnectionsRepo"
+
+        private const val DEFAULT_SUB_ID = 1
+    }
+}
+
+private fun Mobile.toMobileSubscriptionModel(): MobileSubscriptionModel {
+    return MobileSubscriptionModel(
+        isEmergencyOnly = false, // TODO(b/261029387): not yet supported
+        isGsm = false, // TODO(b/261029387): not yet supported
+        cdmaLevel = level ?: 0,
+        primaryLevel = level ?: 0,
+        dataConnectionState = DataConnectionState.Connected, // TODO(b/261029387): not yet supported
+        dataActivityDirection = activity,
+        carrierNetworkChangeActive = carrierNetworkChange,
+        // TODO(b/261185097): once mobile mappings can be mocked at this layer, we can build our
+        //  own demo map
+        resolvedNetworkType = dataType.toResolvedNetworkType()
+    )
+}
+
+@Annotation.NetworkType
+private fun SignalIcon.MobileIconGroup?.toNetworkType(): Int =
+    when (this) {
+        TelephonyIcons.THREE_G -> NETWORK_TYPE_GSM
+        TelephonyIcons.LTE -> NETWORK_TYPE_LTE
+        TelephonyIcons.FOUR_G -> NETWORK_TYPE_UMTS
+        TelephonyIcons.NR_5G -> NETWORK_TYPE_NR
+        TelephonyIcons.NR_5G_PLUS -> OVERRIDE_NETWORK_TYPE_NR_ADVANCED
+        else -> NETWORK_TYPE_UNKNOWN
+    }
+
+private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType =
+    when (this) {
+        TelephonyIcons.NR_5G_PLUS -> OverrideNetworkType(toNetworkType())
+        else -> DefaultNetworkType(toNetworkType())
+    }
+
+class DemoMobileConnectionRepository(val subId: Int) : MobileConnectionRepository {
+    override val subscriptionModelFlow = MutableStateFlow(MobileSubscriptionModel())
+
+    override val dataEnabled = MutableStateFlow(true)
+
+    override val isDefaultDataSubscription = MutableStateFlow(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
new file mode 100644
index 0000000..da55787
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
+
+import android.os.Bundle
+import android.telephony.Annotation.DataActivityType
+import android.telephony.TelephonyManager.DATA_ACTIVITY_IN
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
+import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+
+/**
+ * Data source that can map from demo mode commands to inputs into the
+ * [DemoMobileConnectionsRepository]'s flows
+ */
+@SysUISingleton
+class DemoModeMobileConnectionDataSource
+@Inject
+constructor(
+    demoModeController: DemoModeController,
+    @Application scope: CoroutineScope,
+) {
+    private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow {
+        val callback =
+            object : DemoMode {
+                override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK)
+
+                override fun dispatchDemoCommand(command: String, args: Bundle) {
+                    trySend(args)
+                }
+
+                override fun onDemoModeFinished() {
+                    // Handled elsewhere
+                }
+
+                override fun onDemoModeStarted() {
+                    // Handled elsewhere
+                }
+            }
+
+        demoModeController.addCallback(callback)
+        awaitClose { demoModeController.removeCallback(callback) }
+    }
+
+    // If the args contains "mobile", then all of the args are relevant. It's just the way demo mode
+    // commands work and it's a little silly
+    private val _mobileCommands = demoCommandStream.map { args -> args.toMobileEvent() }
+    val mobileEvents = _mobileCommands.shareIn(scope, SharingStarted.WhileSubscribed())
+
+    private fun Bundle.toMobileEvent(): FakeNetworkEventModel? {
+        val mobile = getString("mobile") ?: return null
+        return if (mobile == "show") {
+            activeMobileEvent()
+        } else {
+            MobileDisabled(subId = getString("slot")?.toInt())
+        }
+    }
+
+    /** Parse a valid mobile command string into a network event */
+    private fun Bundle.activeMobileEvent(): Mobile {
+        // There are many key/value pairs supported by mobile demo mode. Bear with me here
+        val level = getString("level")?.toInt()
+        val dataType = getString("datatype")?.toDataType()
+        val slot = getString("slot")?.toInt()
+        val carrierId = getString("carrierid")?.toInt()
+        val inflateStrength = getString("inflate")?.toBoolean()
+        val activity = getString("activity")?.toActivity()
+        val carrierNetworkChange = getString("carriernetworkchange") == "show"
+
+        return Mobile(
+            level = level,
+            dataType = dataType,
+            subId = slot,
+            carrierId = carrierId,
+            inflateStrength = inflateStrength,
+            activity = activity,
+            carrierNetworkChange = carrierNetworkChange,
+        )
+    }
+}
+
+private fun String.toDataType(): MobileIconGroup =
+    when (this) {
+        "1x" -> TelephonyIcons.ONE_X
+        "3g" -> TelephonyIcons.THREE_G
+        "4g" -> TelephonyIcons.FOUR_G
+        "4g+" -> TelephonyIcons.FOUR_G_PLUS
+        "5g" -> TelephonyIcons.NR_5G
+        "5ge" -> TelephonyIcons.LTE_CA_5G_E
+        "5g+" -> TelephonyIcons.NR_5G_PLUS
+        "e" -> TelephonyIcons.E
+        "g" -> TelephonyIcons.G
+        "h" -> TelephonyIcons.H
+        "h+" -> TelephonyIcons.H_PLUS
+        "lte" -> TelephonyIcons.LTE
+        "lte+" -> TelephonyIcons.LTE_PLUS
+        "dis" -> TelephonyIcons.DATA_DISABLED
+        "not" -> TelephonyIcons.NOT_DEFAULT_DATA
+        else -> TelephonyIcons.UNKNOWN
+    }
+
+@DataActivityType
+private fun String.toActivity(): Int =
+    when (this) {
+        "inout" -> DATA_ACTIVITY_INOUT
+        "in" -> DATA_ACTIVITY_IN
+        "out" -> DATA_ACTIVITY_OUT
+        else -> DATA_ACTIVITY_NONE
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
new file mode 100644
index 0000000..3f3acaf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model
+
+import android.telephony.Annotation.DataActivityType
+import com.android.settingslib.SignalIcon
+
+/**
+ * Model for the demo commands, ported from [NetworkControllerImpl]
+ *
+ * Nullable fields represent optional command line arguments
+ */
+sealed interface FakeNetworkEventModel {
+    data class Mobile(
+        val level: Int?,
+        val dataType: SignalIcon.MobileIconGroup?,
+        // Null means the default (chosen by the repository)
+        val subId: Int?,
+        val carrierId: Int?,
+        val inflateStrength: Boolean?,
+        @DataActivityType val activity: Int?,
+        val carrierNetworkChange: Boolean,
+    ) : FakeNetworkEventModel
+
+    data class MobileDisabled(
+        // Null means the default (chosen by the repository)
+        val subId: Int?
+    ) : FakeNetworkEventModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
new file mode 100644
index 0000000..4c1cf4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.content.Context
+import android.database.ContentObserver
+import android.provider.Settings.Global
+import android.telephony.CellSignalStrength
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
+import android.telephony.TelephonyManager
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.util.settings.GlobalSettings
+import java.lang.IllegalStateException
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class MobileConnectionRepositoryImpl(
+    private val context: Context,
+    private val subId: Int,
+    private val telephonyManager: TelephonyManager,
+    private val globalSettings: GlobalSettings,
+    defaultDataSubId: StateFlow<Int>,
+    globalMobileDataSettingChangedEvent: Flow<Unit>,
+    bgDispatcher: CoroutineDispatcher,
+    logger: ConnectivityPipelineLogger,
+    scope: CoroutineScope,
+) : MobileConnectionRepository {
+    init {
+        if (telephonyManager.subscriptionId != subId) {
+            throw IllegalStateException(
+                "TelephonyManager should be created with subId($subId). " +
+                    "Found ${telephonyManager.subscriptionId} instead."
+            )
+        }
+    }
+
+    private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+
+    override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
+        var state = MobileSubscriptionModel()
+        conflatedCallbackFlow {
+                // TODO (b/240569788): log all of these into the connectivity logger
+                val callback =
+                    object :
+                        TelephonyCallback(),
+                        TelephonyCallback.ServiceStateListener,
+                        TelephonyCallback.SignalStrengthsListener,
+                        TelephonyCallback.DataConnectionStateListener,
+                        TelephonyCallback.DataActivityListener,
+                        TelephonyCallback.CarrierNetworkListener,
+                        TelephonyCallback.DisplayInfoListener {
+                        override fun onServiceStateChanged(serviceState: ServiceState) {
+                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+                            trySend(state)
+                        }
+
+                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+                            val cdmaLevel =
+                                signalStrength
+                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
+                                    .let { strengths ->
+                                        if (!strengths.isEmpty()) {
+                                            strengths[0].level
+                                        } else {
+                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+                                        }
+                                    }
+
+                            val primaryLevel = signalStrength.level
+
+                            state =
+                                state.copy(
+                                    cdmaLevel = cdmaLevel,
+                                    primaryLevel = primaryLevel,
+                                    isGsm = signalStrength.isGsm,
+                                )
+                            trySend(state)
+                        }
+
+                        override fun onDataConnectionStateChanged(
+                            dataState: Int,
+                            networkType: Int
+                        ) {
+                            state =
+                                state.copy(dataConnectionState = dataState.toDataConnectionType())
+                            trySend(state)
+                        }
+
+                        override fun onDataActivity(direction: Int) {
+                            state = state.copy(dataActivityDirection = direction)
+                            trySend(state)
+                        }
+
+                        override fun onCarrierNetworkChange(active: Boolean) {
+                            state = state.copy(carrierNetworkChangeActive = active)
+                            trySend(state)
+                        }
+
+                        override fun onDisplayInfoChanged(
+                            telephonyDisplayInfo: TelephonyDisplayInfo
+                        ) {
+                            val networkType =
+                                if (
+                                    telephonyDisplayInfo.overrideNetworkType ==
+                                        OVERRIDE_NETWORK_TYPE_NONE
+                                ) {
+                                    DefaultNetworkType(telephonyDisplayInfo.networkType)
+                                } else {
+                                    OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
+                                }
+                            state = state.copy(resolvedNetworkType = networkType)
+                            trySend(state)
+                        }
+                    }
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .onEach { telephonyCallbackEvent.tryEmit(Unit) }
+            .logOutputChange(logger, "MobileSubscriptionModel")
+            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+    }
+
+    /** Produces whenever the mobile data setting changes for this subId */
+    private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
+        val observer =
+            object : ContentObserver(null) {
+                override fun onChange(selfChange: Boolean) {
+                    trySend(Unit)
+                }
+            }
+
+        globalSettings.registerContentObserver(
+            globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
+            /* notifyForDescendants */ true,
+            observer
+        )
+
+        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+    }
+
+    /**
+     * There are a few cases where we will need to poll [TelephonyManager] so we can update some
+     * internal state where callbacks aren't provided. Any of those events should be merged into
+     * this flow, which can be used to trigger the polling.
+     */
+    private val telephonyPollingEvent: Flow<Unit> =
+        merge(
+            telephonyCallbackEvent,
+            localMobileDataSettingChangedEvent,
+            globalMobileDataSettingChangedEvent,
+        )
+
+    override val dataEnabled: StateFlow<Boolean> =
+        telephonyPollingEvent
+            .mapLatest { dataConnectionAllowed() }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
+
+    private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
+
+    override val isDefaultDataSubscription: StateFlow<Boolean> =
+        defaultDataSubId
+            .mapLatest { it == subId }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
+
+    class Factory
+    @Inject
+    constructor(
+        private val context: Context,
+        private val telephonyManager: TelephonyManager,
+        private val logger: ConnectivityPipelineLogger,
+        private val globalSettings: GlobalSettings,
+        @Background private val bgDispatcher: CoroutineDispatcher,
+        @Application private val scope: CoroutineScope,
+    ) {
+        fun build(
+            subId: Int,
+            defaultDataSubId: StateFlow<Int>,
+            globalMobileDataSettingChangedEvent: Flow<Unit>,
+        ): MobileConnectionRepository {
+            return MobileConnectionRepositoryImpl(
+                context,
+                subId,
+                telephonyManager.createForSubscriptionId(subId),
+                globalSettings,
+                defaultDataSubId,
+                globalMobileDataSettingChangedEvent,
+                bgDispatcher,
+                logger,
+                scope,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
new file mode 100644
index 0000000..08d6010
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.provider.Settings.Global.MOBILE_DATA
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.android.internal.telephony.PhoneConstants
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.settings.GlobalSettings
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileConnectionsRepositoryImpl
+@Inject
+constructor(
+    private val connectivityManager: ConnectivityManager,
+    private val subscriptionManager: SubscriptionManager,
+    private val telephonyManager: TelephonyManager,
+    private val logger: ConnectivityPipelineLogger,
+    broadcastDispatcher: BroadcastDispatcher,
+    private val globalSettings: GlobalSettings,
+    private val context: Context,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
+    private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+) : MobileConnectionsRepository {
+    private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+
+    /**
+     * State flow that emits the set of mobile data subscriptions, each represented by its own
+     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
+     * info object, but for now we keep track of the infos themselves.
+     */
+    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                        override fun onSubscriptionsChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                subscriptionManager.addOnSubscriptionsChangedListener(
+                    bgDispatcher.asExecutor(),
+                    callback,
+                )
+
+                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+            }
+            .mapLatest { fetchSubscriptionsList() }
+            .onEach { infos -> dropUnusedReposFromCache(infos) }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+
+    /** StateFlow that keeps track of the current active mobile data subscription */
+    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
+                            trySend(subId)
+                        }
+                    }
+
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
+
+    private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
+        MutableSharedFlow(extraBufferCapacity = 1)
+
+    override val defaultDataSubId: StateFlow<Int> =
+        broadcastDispatcher
+            .broadcastFlow(
+                IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+            ) { intent, _ ->
+                intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
+            }
+            .distinctUntilChanged()
+            .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                SubscriptionManager.getDefaultDataSubscriptionId()
+            )
+
+    private val carrierConfigChangedEvent =
+        broadcastDispatcher.broadcastFlow(
+            IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+        )
+
+    /**
+     * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
+     * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
+     * config, so this will apply to every icon that we care about.
+     *
+     * Relevant bits in the config are things like
+     * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
+     *
+     * This flow will produce whenever the default data subscription or the carrier config changes.
+     */
+    override val defaultDataSubRatConfig: StateFlow<Config> =
+        merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
+            .mapLatest { Config.readConfig(context) }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                initialValue = Config.readConfig(context)
+            )
+
+    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+        if (!isValidSubId(subId)) {
+            throw IllegalArgumentException(
+                "subscriptionId $subId is not in the list of valid subscriptions"
+            )
+        }
+
+        return subIdRepositoryCache[subId]
+            ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
+    }
+
+    /**
+     * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
+     * connection repositories also observe the URI for [MOBILE_DATA] + subId.
+     */
+    override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
+        val observer =
+            object : ContentObserver(null) {
+                override fun onChange(selfChange: Boolean) {
+                    trySend(Unit)
+                }
+            }
+
+        globalSettings.registerContentObserver(
+            globalSettings.getUriFor(MOBILE_DATA),
+            true,
+            observer
+        )
+
+        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+    }
+
+    @SuppressLint("MissingPermission")
+    override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+                        override fun onLost(network: Network) {
+                            // Send a disconnected model when lost. Maybe should create a sealed
+                            // type or null here?
+                            trySend(MobileConnectivityModel())
+                        }
+
+                        override fun onCapabilitiesChanged(
+                            network: Network,
+                            caps: NetworkCapabilities
+                        ) {
+                            trySend(
+                                MobileConnectivityModel(
+                                    isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
+                                    isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
+                                )
+                            )
+                        }
+                    }
+
+                connectivityManager.registerDefaultNetworkCallback(callback)
+
+                awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
+
+    private fun isValidSubId(subId: Int): Boolean {
+        subscriptionsFlow.value.forEach {
+            if (it.subscriptionId == subId) {
+                return true
+            }
+        }
+
+        return false
+    }
+
+    @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
+
+    private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
+        return mobileConnectionRepositoryFactory.build(
+            subId,
+            defaultDataSubId,
+            globalMobileDataSettingChangedEvent,
+        )
+    }
+
+    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
+        // Remove any connection repository from the cache that isn't in the new set of IDs. They
+        // will get garbage collected once their subscribers go away
+        val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
+
+        subIdRepositoryCache.keys.forEach {
+            if (!currentValidSubscriptionIds.contains(it)) {
+                subIdRepositoryCache.remove(it)
+            }
+        }
+    }
+
+    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
+        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index c7e0ce1..d9487bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.ui
 
+import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.phone.StatusBarIconController
@@ -29,9 +30,10 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /**
  * This class is intended to provide a context to collect on the
@@ -50,9 +52,9 @@
     interactor: MobileIconsInteractor,
     private val iconController: StatusBarIconController,
     private val iconsViewModelFactory: MobileIconsViewModel.Factory,
-    @Application scope: CoroutineScope,
+    @Application private val scope: CoroutineScope,
     private val statusBarPipelineFlags: StatusBarPipelineFlags,
-) {
+) : CoreStartable {
     private val mobileSubIds: Flow<List<Int>> =
         interactor.filteredSubscriptions.mapLatest { infos ->
             infos.map { subscriptionInfo -> subscriptionInfo.subscriptionId }
@@ -66,18 +68,19 @@
      * NOTE: this should go away as the view presenter learns more about this data pipeline
      */
     private val mobileSubIdsState: StateFlow<List<Int>> =
-        mobileSubIds
-            .onEach {
-                // Only notify the icon controller if we want to *render* the new icons.
-                // Note that this flow may still run if
-                // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to
-                // get the logging data without rendering.
-                if (statusBarPipelineFlags.useNewMobileIcons()) {
-                    // Notify the icon controller here so that it knows to add icons
-                    iconController.setNewMobileIconSubIds(it)
-                }
+        mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+
+    override fun start() {
+        // Only notify the icon controller if we want to *render* the new icons.
+        // Note that this flow may still run if
+        // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to
+        // get the logging data without rendering.
+        if (statusBarPipelineFlags.useNewMobileIcons()) {
+            scope.launch {
+                mobileSubIds.collectLatest { iconController.setNewMobileIconSubIds(it) }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+        }
+    }
 
     /**
      * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
index b816364..5223760 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import javax.inject.Inject
@@ -73,7 +74,9 @@
                         // Note that this flow may still run if
                         // [statusBarPipelineFlags.runNewWifiIconBackend] is true because we may
                         // want to get the logging data without rendering.
-                        if (wifiIcon != null && statusBarPipelineFlags.useNewWifiIcon()) {
+                        if (
+                            wifiIcon is WifiIcon.Visible && statusBarPipelineFlags.useNewWifiIcon()
+                        ) {
                             iconController.setNewWifiIcon()
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index 345f8cb..f5b5950 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 import kotlinx.coroutines.InternalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -92,8 +93,10 @@
 
                 launch {
                     viewModel.wifiIcon.collect { wifiIcon ->
-                        view.isVisible = wifiIcon != null
-                        wifiIcon?.let { IconViewBinder.bind(wifiIcon, iconView) }
+                        view.isVisible = wifiIcon is WifiIcon.Visible
+                        if (wifiIcon is WifiIcon.Visible) {
+                            IconViewBinder.bind(wifiIcon.icon, iconView)
+                        }
                     }
                 }
 
@@ -135,7 +138,7 @@
 
         return object : Binding {
             override fun getShouldIconBeVisible(): Boolean {
-                return viewModel.wifiIcon.value != null
+                return viewModel.wifiIcon.value is WifiIcon.Visible
             }
 
             override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
new file mode 100644
index 0000000..e491d2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/model/WifiIcon.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.ui.model
+
+import android.annotation.DrawableRes
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+
+/** Represents the various states of the wifi icon. */
+sealed interface WifiIcon : Diffable<WifiIcon> {
+    /** Represents a wifi icon that should be hidden (not visible). */
+    object Hidden : WifiIcon {
+        override fun toString() = "hidden"
+    }
+
+    /**
+     * Represents a visible wifi icon that uses [res] as its image and [contentDescription] as its
+     * description.
+     */
+    class Visible(
+        @DrawableRes res: Int,
+        val contentDescription: ContentDescription.Loaded,
+    ) : WifiIcon {
+        val icon = Icon.Resource(res, contentDescription)
+
+        override fun toString() = contentDescription.description.toString()
+    }
+
+    override fun logDiffs(prevVal: WifiIcon, row: TableRowLogger) {
+        if (prevVal.toString() != toString()) {
+            row.logChange(COL_ICON, toString())
+        }
+    }
+
+    override fun logFull(row: TableRowLogger) {
+        row.logChange(COL_ICON, toString())
+    }
+}
+
+private const val COL_ICON = "wifiIcon"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
index 95ab251..a29c9b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
 import android.graphics.Color
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
@@ -28,7 +28,7 @@
  */
 class HomeWifiViewModel(
     statusBarPipelineFlags: StatusBarPipelineFlags,
-    wifiIcon: StateFlow<Icon.Resource?>,
+    wifiIcon: StateFlow<WifiIcon>,
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
index 86535d6..1e190fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
@@ -17,15 +17,15 @@
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
 import android.graphics.Color
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
 /** A view model for the wifi icon shown on keyguard (lockscreen). */
 class KeyguardWifiViewModel(
     statusBarPipelineFlags: StatusBarPipelineFlags,
-    wifiIcon: StateFlow<Icon.Resource?>,
+    wifiIcon: StateFlow<WifiIcon>,
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
index 7cbdf5d..e35a8fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
 import android.graphics.Color
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.flowOf
@@ -33,8 +33,8 @@
     statusBarPipelineFlags: StatusBarPipelineFlags,
     debugTint: Int,
 
-    /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
-    val wifiIcon: StateFlow<Icon.Resource?>,
+    /** The wifi icon that should be displayed. */
+    val wifiIcon: StateFlow<WifiIcon>,
 
     /** True if the activity in view should be visible. */
     val isActivityInViewVisible: Flow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
index fd54c5f..18e62b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
@@ -17,15 +17,15 @@
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
 import android.graphics.Color
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
 /** A view model for the wifi icon shown in quick settings (when the shade is pulled down). */
 class QsWifiViewModel(
     statusBarPipelineFlags: StatusBarPipelineFlags,
-    wifiIcon: StateFlow<Icon.Resource?>,
+    wifiIcon: StateFlow<WifiIcon>,
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 0782bbb..ec7ba65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -17,16 +17,18 @@
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
 import android.content.Context
-import androidx.annotation.DrawableRes
 import androidx.annotation.StringRes
 import androidx.annotation.VisibleForTesting
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
 import com.android.systemui.R
 import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
@@ -71,50 +73,39 @@
     connectivityConstants: ConnectivityConstants,
     private val context: Context,
     logger: ConnectivityPipelineLogger,
+    @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
     interactor: WifiInteractor,
     @Application private val scope: CoroutineScope,
     statusBarPipelineFlags: StatusBarPipelineFlags,
     wifiConstants: WifiConstants,
 ) {
-    /**
-     * Returns the drawable resource ID to use for the wifi icon based on the given network.
-     * Null if we can't compute the icon.
-     */
-    @DrawableRes
-    private fun WifiNetworkModel.iconResId(): Int? {
+    /** Returns the icon to use based on the given network. */
+    private fun WifiNetworkModel.icon(): WifiIcon {
         return when (this) {
-            is WifiNetworkModel.CarrierMerged -> null
-            is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK
-            is WifiNetworkModel.Active ->
-                when {
-                    this.level == null -> null
-                    this.isValidated -> WIFI_FULL_ICONS[this.level]
-                    else -> WIFI_NO_INTERNET_ICONS[this.level]
-                }
-        }
-    }
-
-    /**
-     * Returns the content description for the wifi icon based on the given network.
-     * Null if we can't compute the content description.
-     */
-    private fun WifiNetworkModel.contentDescription(): ContentDescription? {
-        return when (this) {
-            is WifiNetworkModel.CarrierMerged -> null
-            is WifiNetworkModel.Inactive ->
+            is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden
+            is WifiNetworkModel.Inactive -> WifiIcon.Visible(
+                res = WIFI_NO_NETWORK,
                 ContentDescription.Loaded(
                     "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}"
                 )
+            )
             is WifiNetworkModel.Active ->
                 when (this.level) {
-                    null -> null
+                    null -> WifiIcon.Hidden
                     else -> {
                         val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level])
                         when {
-                            this.isValidated -> ContentDescription.Loaded(levelDesc)
+                            this.isValidated ->
+                                WifiIcon.Visible(
+                                    WIFI_FULL_ICONS[this.level],
+                                    ContentDescription.Loaded(levelDesc)
+                                )
                             else ->
-                                ContentDescription.Loaded(
-                                    "$levelDesc,${context.getString(NO_INTERNET)}"
+                                WifiIcon.Visible(
+                                    WIFI_NO_INTERNET_ICONS[this.level],
+                                    ContentDescription.Loaded(
+                                        "$levelDesc,${context.getString(NO_INTERNET)}"
+                                    )
                                 )
                         }
                     }
@@ -122,8 +113,8 @@
         }
     }
 
-    /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
-    private val wifiIcon: StateFlow<Icon.Resource?> =
+    /** The wifi icon that should be displayed. */
+    private val wifiIcon: StateFlow<WifiIcon> =
         combine(
             interactor.isEnabled,
             interactor.isDefault,
@@ -131,22 +122,29 @@
             interactor.wifiNetwork,
         ) { isEnabled, isDefault, isForceHidden, wifiNetwork ->
             if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
-                return@combine null
+                return@combine WifiIcon.Hidden
             }
 
-            val iconResId = wifiNetwork.iconResId() ?: return@combine null
-            val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription())
+            val icon = wifiNetwork.icon()
 
             return@combine when {
                 isDefault -> icon
                 wifiConstants.alwaysShowIconIfEnabled -> icon
                 !connectivityConstants.hasDataCapabilities -> icon
                 wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon
-                else -> null
+                else -> WifiIcon.Hidden
             }
         }
-                .logOutputChange(logger, "icon") { icon -> icon?.contentDescription.toString() }
-                .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
+            .logDiffsForTable(
+                wifiTableLogBuffer,
+                columnPrefix = "",
+                initialValue = WifiIcon.Hidden,
+            )
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = WifiIcon.Hidden
+            )
 
     /** The wifi activity status. Null if we shouldn't display the activity status. */
     private val activity: Flow<WifiActivityModel?> =
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index a9d05d1..ea40208 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -105,8 +105,9 @@
      *
      * This method handles inflating and attaching the view, then delegates to [updateView] to
      * display the correct information in the view.
+     * @param onViewTimeout a runnable that runs after the view timeout.
      */
-    fun displayView(newInfo: T) {
+    fun displayView(newInfo: T, onViewTimeout: Runnable? = null) {
         val currentDisplayInfo = displayInfo
 
         // Update our list of active devices by removing it if necessary, then adding back at the
@@ -173,7 +174,10 @@
             cancelViewTimeout?.run()
         }
         cancelViewTimeout = mainExecutor.executeDelayed(
-            { removeView(id, REMOVAL_REASON_TIMEOUT) },
+            {
+                removeView(id, REMOVAL_REASON_TIMEOUT)
+                onViewTimeout?.run()
+            },
             timeout.toLong()
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 162c915..b2ec27c 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -40,6 +40,8 @@
 import com.android.systemui.statusbar.LightRevealEffect
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.LinearLightRevealEffect
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.FOLD
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.UNFOLD
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
 import com.android.systemui.unfold.updates.RotationChangeProvider
 import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
@@ -125,7 +127,7 @@
         try {
             // Add the view only if we are unfolding and this is the first screen on
             if (!isFolded && !isUnfoldHandled && contentResolver.areAnimationsEnabled()) {
-                executeInBackground { addView(onOverlayReady) }
+                executeInBackground { addOverlay(onOverlayReady, reason = UNFOLD) }
                 isUnfoldHandled = true
             } else {
                 // No unfold transition, immediately report that overlay is ready
@@ -137,7 +139,7 @@
         }
     }
 
-    private fun addView(onOverlayReady: Runnable? = null) {
+    private fun addOverlay(onOverlayReady: Runnable? = null, reason: AddOverlayReason) {
         if (!::wwm.isInitialized) {
             // Surface overlay is not created yet on the first SysUI launch
             onOverlayReady?.run()
@@ -152,7 +154,10 @@
             LightRevealScrim(context, null).apply {
                 revealEffect = createLightRevealEffect()
                 isScrimOpaqueChangedListener = Consumer {}
-                revealAmount = 0f
+                revealAmount = when (reason) {
+                    FOLD -> TRANSPARENT
+                    UNFOLD -> BLACK
+                }
             }
 
         val params = getLayoutParams()
@@ -228,7 +233,7 @@
     }
 
     private fun getUnfoldedDisplayInfo(): DisplayInfo =
-        displayManager.displays
+        displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
             .asSequence()
             .map { DisplayInfo().apply { it.getDisplayInfo(this) } }
             .filter { it.type == Display.TYPE_INTERNAL }
@@ -247,7 +252,7 @@
         override fun onTransitionStarted() {
             // Add view for folding case (when unfolding the view is added earlier)
             if (scrimView == null) {
-                executeInBackground { addView() }
+                executeInBackground { addOverlay(reason = FOLD) }
             }
             // Disable input dispatching during transition.
             InputManager.getInstance().cancelCurrentTouch()
@@ -294,11 +299,17 @@
             }
         )
 
+    private enum class AddOverlayReason { FOLD, UNFOLD }
+
     private companion object {
-        private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
+        const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
 
         // Put the unfold overlay below the rotation animation screenshot to hide the moment
         // when it is rotated but the rotation of the other windows hasn't happen yet
-        private const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
+        const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
+
+        // constants for revealAmount.
+        const val TRANSPARENT = 1f
+        const val BLACK = 0f
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
index 0f3eddf..bff6132d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
@@ -50,13 +50,6 @@
     }
 
     /**
-     * Wrapped version of {@link DeviceConfig#enforceReadPermission}.
-     */
-    public void enforceReadPermission(String namespace) {
-        DeviceConfig.enforceReadPermission(namespace);
-    }
-
-    /**
      * Wrapped version of {@link DeviceConfig#getBoolean}.
      */
     public boolean getBoolean(
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 02738d5..8ef98f0 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -253,6 +253,12 @@
                 splitScreen.onFinishedWakingUp();
             }
         });
+        mCommandQueue.addCallback(new CommandQueue.Callbacks() {
+            @Override
+            public void goToFullscreenFromSplit() {
+                splitScreen.goToFullscreenFromSplit();
+            }
+        });
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index ffd95f4..d20be56 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -19,6 +19,7 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
 import androidx.test.filters.SmallTest
 import com.android.internal.util.LatencyTracker
 import com.android.internal.widget.LockPatternUtils
@@ -43,6 +44,8 @@
     @Mock
     private lateinit var keyguardPasswordView: KeyguardPasswordView
     @Mock
+    private lateinit var passwordEntry: EditText
+    @Mock
     lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock
     lateinit var securityMode: KeyguardSecurityModel.SecurityMode
@@ -81,6 +84,9 @@
         ).thenReturn(mKeyguardMessageArea)
         Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea))
             .thenReturn(mKeyguardMessageAreaController)
+        Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry)
+        Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)
+        ).thenReturn(passwordEntry)
         keyguardPasswordViewController = KeyguardPasswordViewController(
             keyguardPasswordView,
             keyguardUpdateMonitor,
@@ -103,7 +109,10 @@
         Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
         Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
         keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
-        keyguardPasswordView.post { verify(keyguardPasswordView).requestFocus() }
+        keyguardPasswordView.post {
+            verify(keyguardPasswordView).requestFocus()
+            verify(keyguardPasswordView).showKeyboard()
+        }
     }
 
     @Test
@@ -115,6 +124,15 @@
     }
 
     @Test
+    fun testHideKeyboardWhenOnPause() {
+        keyguardPasswordViewController.onPause()
+        keyguardPasswordView.post {
+            verify(keyguardPasswordView).clearFocus()
+            verify(keyguardPasswordView).hideKeyboard()
+        }
+    }
+
+    @Test
     fun startAppearAnimation() {
         keyguardPasswordViewController.startAppearAnimation()
         verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
diff --git a/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
similarity index 94%
rename from packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
index 389eed0..2c680be 100644
--- a/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.animation
 
 import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
 import java.lang.reflect.Modifier
 import junit.framework.Assert.assertEquals
 import org.junit.Test
@@ -25,7 +26,7 @@
 
 @SmallTest
 @RunWith(JUnit4::class)
-class InterpolatorsAndroidXTest {
+class InterpolatorsAndroidXTest : SysuiTestCase() {
 
     @Test
     fun testInterpolatorsAndInterpolatorsAndroidXPublicMethodsAreEqual() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index e679b13..d965e33 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -16,15 +16,26 @@
 
 package com.android.systemui.controls.ui
 
+import android.app.PendingIntent
 import android.content.ComponentName
 import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.os.UserHandle
+import android.service.controls.ControlsProviderService
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.ControlsMetricsLogger
+import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.CustomIconCache
+import com.android.systemui.controls.FakeControlsSettingsRepository
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.controller.StructureInfo
 import com.android.systemui.controls.management.ControlsListingController
@@ -38,19 +49,26 @@
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.wm.shell.TaskView
 import com.android.wm.shell.TaskViewFactory
 import com.google.common.truth.Truth.assertThat
 import dagger.Lazy
 import java.util.Optional
+import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.anyString
-import org.mockito.Mockito.mock
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -70,9 +88,9 @@
     @Mock lateinit var userFileManager: UserFileManager
     @Mock lateinit var userTracker: UserTracker
     @Mock lateinit var taskViewFactory: TaskViewFactory
-    @Mock lateinit var activityContext: Context
     @Mock lateinit var dumpManager: DumpManager
     val sharedPreferences = FakeSharedPreferences()
+    lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
 
     var uiExecutor = FakeExecutor(FakeSystemClock())
     var bgExecutor = FakeExecutor(FakeSystemClock())
@@ -83,6 +101,17 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
+        controlsSettingsRepository = FakeControlsSettingsRepository()
+
+        // This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we
+        // need to clone it once so we don't modify the original one.
+        mContext.addMockSystemService(
+            Context.LAYOUT_INFLATER_SERVICE,
+            mContext.baseContext
+                .getSystemService(LayoutInflater::class.java)!!
+                .cloneInContext(mContext)
+        )
+
         parent = FrameLayout(mContext)
 
         underTest =
@@ -100,6 +129,7 @@
                 userFileManager,
                 userTracker,
                 Optional.of(taskViewFactory),
+                controlsSettingsRepository,
                 dumpManager
             )
         `when`(
@@ -113,11 +143,12 @@
         `when`(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt()))
             .thenReturn(sharedPreferences)
         `when`(userTracker.userId).thenReturn(0)
+        `when`(userTracker.userHandle).thenReturn(UserHandle.of(0))
     }
 
     @Test
     fun testGetPreferredStructure() {
-        val structureInfo = mock(StructureInfo::class.java)
+        val structureInfo = mock<StructureInfo>()
         underTest.getPreferredSelectedItem(listOf(structureInfo))
         verify(userFileManager)
             .getSharedPreferences(
@@ -189,14 +220,195 @@
     @Test
     fun testPanelDoesNotRefreshControls() {
         val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        setUpPanel(panel)
+
+        underTest.show(parent, {}, context)
+        verify(controlsController, never()).refreshStatus(any(), any())
+    }
+
+    @Test
+    fun testPanelCallsTaskViewFactoryCreate() {
+        mockLayoutInflater()
+        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        val serviceInfo = setUpPanel(panel)
+
+        underTest.show(parent, {}, context)
+
+        val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>()
+
+        verify(controlsListingController).addCallback(capture(captor))
+
+        captor.value.onServicesUpdated(listOf(serviceInfo))
+        FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+        verify(taskViewFactory).create(eq(context), eq(uiExecutor), any())
+    }
+
+    @Test
+    fun testPanelControllerStartActivityWithCorrectArguments() {
+        mockLayoutInflater()
+        controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+
+        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        val serviceInfo = setUpPanel(panel)
+
+        underTest.show(parent, {}, context)
+
+        val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>()
+
+        verify(controlsListingController).addCallback(capture(captor))
+
+        captor.value.onServicesUpdated(listOf(serviceInfo))
+        FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+        val pendingIntent = verifyPanelCreatedAndStartTaskView()
+
+        with(pendingIntent) {
+            assertThat(isActivity).isTrue()
+            assertThat(intent.component).isEqualTo(serviceInfo.panelActivity)
+            assertThat(
+                    intent.getBooleanExtra(
+                        ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+                        false
+                    )
+                )
+                .isTrue()
+        }
+    }
+
+    @Test
+    fun testPendingIntentExtrasAreModified() {
+        mockLayoutInflater()
+        controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+
+        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        val serviceInfo = setUpPanel(panel)
+
+        underTest.show(parent, {}, context)
+
+        val captor = argumentCaptor<ControlsListingController.ControlsListingCallback>()
+
+        verify(controlsListingController).addCallback(capture(captor))
+
+        captor.value.onServicesUpdated(listOf(serviceInfo))
+        FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+        val pendingIntent = verifyPanelCreatedAndStartTaskView()
+        assertThat(
+                pendingIntent.intent.getBooleanExtra(
+                    ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+                    false
+                )
+            )
+            .isTrue()
+
+        underTest.hide()
+
+        clearInvocations(controlsListingController, taskViewFactory)
+        controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(false)
+        underTest.show(parent, {}, context)
+
+        verify(controlsListingController).addCallback(capture(captor))
+        captor.value.onServicesUpdated(listOf(serviceInfo))
+        FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+        val newPendingIntent = verifyPanelCreatedAndStartTaskView()
+        assertThat(
+                newPendingIntent.intent.getBooleanExtra(
+                    ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS,
+                    false
+                )
+            )
+            .isFalse()
+    }
+
+    private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo {
+        val activity = ComponentName("pkg", "activity")
         sharedPreferences
             .edit()
             .putString("controls_component", panel.componentName.flattenToString())
             .putString("controls_structure", panel.appName.toString())
             .putBoolean("controls_is_panel", true)
             .commit()
+        return ControlsServiceInfo(panel.componentName, panel.appName, activity)
+    }
 
-        underTest.show(parent, {}, activityContext)
-        verify(controlsController, never()).refreshStatus(any(), any())
+    private fun verifyPanelCreatedAndStartTaskView(): PendingIntent {
+        val taskViewConsumerCaptor = argumentCaptor<Consumer<TaskView>>()
+        verify(taskViewFactory).create(eq(context), eq(uiExecutor), capture(taskViewConsumerCaptor))
+
+        val taskView: TaskView = mock {
+            `when`(this.post(any())).thenAnswer {
+                uiExecutor.execute(it.arguments[0] as Runnable)
+                true
+            }
+        }
+        // calls PanelTaskViewController#launchTaskView
+        taskViewConsumerCaptor.value.accept(taskView)
+        val listenerCaptor = argumentCaptor<TaskView.Listener>()
+        verify(taskView).setListener(any(), capture(listenerCaptor))
+        listenerCaptor.value.onInitialized()
+        FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
+
+        val pendingIntentCaptor = argumentCaptor<PendingIntent>()
+        verify(taskView).startActivity(capture(pendingIntentCaptor), any(), any(), any())
+        return pendingIntentCaptor.value
+    }
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        panelComponentName: ComponentName? = null
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return spy(ControlsServiceInfo(mContext, serviceInfo)).apply {
+            `when`(loadLabel()).thenReturn(label)
+            `when`(loadIcon()).thenReturn(mock())
+            `when`(panelActivity).thenReturn(panelComponentName)
+        }
+    }
+
+    private fun mockLayoutInflater() {
+        LayoutInflater.from(context)
+            .setPrivateFactory(
+                object : LayoutInflater.Factory2 {
+                    override fun onCreateView(
+                        view: View?,
+                        name: String,
+                        context: Context,
+                        attrs: AttributeSet
+                    ): View? {
+                        return onCreateView(name, context, attrs)
+                    }
+
+                    override fun onCreateView(
+                        name: String,
+                        context: Context,
+                        attrs: AttributeSet
+                    ): View? {
+                        if (FrameLayout::class.java.simpleName.equals(name)) {
+                            val mock: FrameLayout = mock {
+                                `when`(this.context).thenReturn(context)
+                                `when`(this.id).thenReturn(R.id.controls_panel)
+                                `when`(this.requireViewById<View>(any())).thenCallRealMethod()
+                                `when`(this.findViewById<View>(R.id.controls_panel))
+                                    .thenReturn(this)
+                                `when`(this.post(any())).thenAnswer {
+                                    uiExecutor.execute(it.arguments[0] as Runnable)
+                                    true
+                                }
+                            }
+                            return mock
+                        } else {
+                            return null
+                        }
+                    }
+                }
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index d17e374..798839d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard;
 
+import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManagerPolicyConstants.OFF_BECAUSE_OF_USER;
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
@@ -34,6 +35,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.IActivityManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.TrustManager;
 import android.os.PowerManager;
@@ -41,6 +43,11 @@
 import android.telephony.TelephonyManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.RemoteAnimationTarget;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -52,21 +59,27 @@
 import com.android.keyguard.mediator.ScreenOnCoordinator;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingCollectorFake;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
@@ -80,8 +93,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import dagger.Lazy;
-
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 @SmallTest
@@ -96,11 +107,15 @@
     private @Mock BroadcastDispatcher mBroadcastDispatcher;
     private @Mock DismissCallbackRegistry mDismissCallbackRegistry;
     private @Mock DumpManager mDumpManager;
+    private @Mock WindowManager mWindowManager;
+    private @Mock IActivityManager mActivityManager;
+    private @Mock ConfigurationController mConfigurationController;
     private @Mock PowerManager mPowerManager;
     private @Mock TrustManager mTrustManager;
     private @Mock UserSwitcherController mUserSwitcherController;
     private @Mock NavigationModeController mNavigationModeController;
     private @Mock KeyguardDisplayManager mKeyguardDisplayManager;
+    private @Mock KeyguardBypassController mKeyguardBypassController;
     private @Mock DozeParameters mDozeParameters;
     private @Mock SysuiStatusBarStateController mStatusBarStateController;
     private @Mock KeyguardStateController mKeyguardStateController;
@@ -110,10 +125,13 @@
     private @Mock InteractionJankMonitor mInteractionJankMonitor;
     private @Mock ScreenOnCoordinator mScreenOnCoordinator;
     private @Mock ShadeController mShadeController;
-    private @Mock Lazy<NotificationShadeWindowController> mNotificationShadeWindowControllerLazy;
+    private NotificationShadeWindowController mNotificationShadeWindowController;
     private @Mock DreamOverlayStateController mDreamOverlayStateController;
     private @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
     private @Mock ScrimController mScrimController;
+    private @Mock SysuiColorExtractor mColorExtractor;
+    private @Mock AuthController mAuthController;
+    private @Mock ShadeExpansionStateManager mShadeExpansionStateManager;
     private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake();
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
 
@@ -130,6 +148,14 @@
         when(mPowerManager.newWakeLock(anyInt(), any())).thenReturn(mock(WakeLock.class));
         when(mInteractionJankMonitor.begin(any(), anyInt())).thenReturn(true);
         when(mInteractionJankMonitor.end(anyInt())).thenReturn(true);
+        final ViewRootImpl testViewRoot = mock(ViewRootImpl.class);
+        when(testViewRoot.getView()).thenReturn(mock(View.class));
+        when(mStatusBarKeyguardViewManager.getViewRootImpl()).thenReturn(testViewRoot);
+        mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(mContext,
+                mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
+                mConfigurationController, mViewMediator, mKeyguardBypassController,
+                mColorExtractor, mDumpManager, mKeyguardStateController,
+                mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager);
 
         createAndStartViewMediator();
     }
@@ -287,6 +313,23 @@
         verify(mCentralSurfaces).updateIsKeyguard();
     }
 
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void testStartKeyguardExitAnimation_expectSurfaceBehindRemoteAnimation() {
+        RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{
+                mock(RemoteAnimationTarget.class)
+        };
+        RemoteAnimationTarget[] wallpapers = new RemoteAnimationTarget[]{
+                mock(RemoteAnimationTarget.class)
+        };
+        IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class);
+
+        mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers,
+                null, callback);
+        TestableLooper.get(this).processAllMessages();
+        assertTrue(mViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehind());
+    }
+
     private void createAndStartViewMediator() {
         mViewMediator = new KeyguardViewMediator(
                 mContext,
@@ -315,7 +358,7 @@
                 mInteractionJankMonitor,
                 mDreamOverlayStateController,
                 () -> mShadeController,
-                mNotificationShadeWindowControllerLazy,
+                () -> mNotificationShadeWindowController,
                 () -> mActivityLaunchAnimator,
                 () -> mScrimController);
         mViewMediator.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index 761773b..fdef344 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -702,7 +702,7 @@
     }
 
     @Test
-    fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToGone() {
+    fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToInvisible() {
         useRealConstraintSets()
 
         val state = mediaData.copy(semanticActions = MediaButton())
@@ -711,7 +711,7 @@
 
         player.bindPlayer(state, PACKAGE)
 
-        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
     }
 
     @Test
@@ -741,7 +741,7 @@
     }
 
     @Test
-    fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToGone() {
+    fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToInvisible() {
         useRealConstraintSets()
 
         val state = mediaData.copy(semanticActions = MediaButton())
@@ -752,7 +752,7 @@
 
         getEnabledChangeListener().onEnabledChanged(enabled = false)
 
-        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index 4437394..311740e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -265,6 +265,8 @@
 
     @Test
     fun commandQueueCallback_transferToReceiverSucceeded_triggersCorrectChip() {
+        displayReceiverTriggered()
+        reset(vibratorHelper)
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
             routeInfo,
@@ -278,13 +280,15 @@
             .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText())
         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
         assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(uiEventLoggerFake.eventId(0))
+        // Event index 1 since initially displaying the triggered chip would also log an event.
+        assertThat(uiEventLoggerFake.eventId(1))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id)
         verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
     }
 
     @Test
     fun transferToReceiverSucceeded_nullUndoCallback_noUndo() {
+        displayReceiverTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
             routeInfo,
@@ -297,6 +301,7 @@
 
     @Test
     fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() {
+        displayReceiverTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
             routeInfo,
@@ -313,6 +318,7 @@
     @Test
     fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
         var undoCallbackCalled = false
+        displayReceiverTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
             routeInfo,
@@ -325,8 +331,9 @@
 
         getChipbarView().getUndoButton().performClick()
 
-        // Event index 1 since initially displaying the succeeded chip would also log an event
-        assertThat(uiEventLoggerFake.eventId(1))
+        // Event index 2 since initially displaying the triggered and succeeded chip would also log
+        // events.
+        assertThat(uiEventLoggerFake.eventId(2))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id)
         assertThat(undoCallbackCalled).isTrue()
         assertThat(getChipbarView().getChipText())
@@ -335,6 +342,8 @@
 
     @Test
     fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() {
+        displayThisDeviceTriggered()
+        reset(vibratorHelper)
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
             routeInfo,
@@ -348,13 +357,15 @@
             .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText())
         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
         assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(uiEventLoggerFake.eventId(0))
+        // Event index 1 since initially displaying the triggered chip would also log an event.
+        assertThat(uiEventLoggerFake.eventId(1))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id)
         verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
     }
 
     @Test
     fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() {
+        displayThisDeviceTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
             routeInfo,
@@ -367,6 +378,7 @@
 
     @Test
     fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() {
+        displayThisDeviceTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
             routeInfo,
@@ -383,6 +395,7 @@
     @Test
     fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
         var undoCallbackCalled = false
+        displayThisDeviceTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
             routeInfo,
@@ -395,8 +408,9 @@
 
         getChipbarView().getUndoButton().performClick()
 
-        // Event index 1 since initially displaying the succeeded chip would also log an event
-        assertThat(uiEventLoggerFake.eventId(1))
+        // Event index 2 since initially displaying the triggered and succeeded chip would also log
+        // events.
+        assertThat(uiEventLoggerFake.eventId(2))
             .isEqualTo(
                 MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id
             )
@@ -407,6 +421,8 @@
 
     @Test
     fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() {
+        displayReceiverTriggered()
+        reset(vibratorHelper)
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED,
             routeInfo,
@@ -421,7 +437,8 @@
         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
         assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
         assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
-        assertThat(uiEventLoggerFake.eventId(0))
+        // Event index 1 since initially displaying the triggered chip would also log an event.
+        assertThat(uiEventLoggerFake.eventId(1))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id)
         verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
@@ -429,6 +446,12 @@
     @Test
     fun commandQueueCallback_transferToThisDeviceFailed_triggersCorrectChip() {
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+            routeInfo,
+            null
+        )
+        reset(vibratorHelper)
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED,
             routeInfo,
             null
@@ -442,7 +465,8 @@
         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
         assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
         assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
-        assertThat(uiEventLoggerFake.eventId(0))
+        // Event index 1 since initially displaying the triggered chip would also log an event.
+        assertThat(uiEventLoggerFake.eventId(1))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id)
         verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
@@ -517,6 +541,166 @@
     }
 
     @Test
+    fun commandQueueCallback_receiverTriggeredThenAlmostStart_invalidTransitionLogged() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+            routeInfo,
+            null
+        )
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+            routeInfo,
+            null
+        )
+
+        verify(logger).logInvalidStateTransitionError(any(), any())
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    @Test
+    fun commandQueueCallback_thisDeviceTriggeredThenAlmostEnd_invalidTransitionLogged() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+            routeInfo,
+            null
+        )
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            routeInfo,
+            null
+        )
+
+        verify(logger).logInvalidStateTransitionError(any(), any())
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    @Test
+    fun commandQueueCallback_receiverSucceededThenReceiverTriggered_invalidTransitionLogged() {
+        displayReceiverTriggered()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            null
+        )
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+            routeInfo,
+            null
+        )
+
+        verify(logger).logInvalidStateTransitionError(any(), any())
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    @Test
+    fun commandQueueCallback_thisDeviceSucceededThenThisDeviceTriggered_invalidTransitionLogged() {
+        displayThisDeviceTriggered()
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            null
+        )
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+            routeInfo,
+            null
+        )
+
+        verify(logger).logInvalidStateTransitionError(any(), any())
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    @Test
+    fun commandQueueCallback_almostStartThenReceiverSucceeded_invalidTransitionLogged() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+            routeInfo,
+            null
+        )
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            null
+        )
+
+        verify(logger).logInvalidStateTransitionError(any(), any())
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    @Test
+    fun commandQueueCallback_almostEndThenThisDeviceSucceeded_invalidTransitionLogged() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            routeInfo,
+            null
+        )
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            null
+        )
+
+        verify(logger).logInvalidStateTransitionError(any(), any())
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    @Test
+    fun commandQueueCallback_AlmostStartThenReceiverFailed_invalidTransitionLogged() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+            routeInfo,
+            null
+        )
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED,
+            routeInfo,
+            null
+        )
+
+        verify(logger).logInvalidStateTransitionError(any(), any())
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    @Test
+    fun commandQueueCallback_almostEndThenThisDeviceFailed_invalidTransitionLogged() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+            routeInfo,
+            null
+        )
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED,
+            routeInfo,
+            null
+        )
+
+        verify(logger).logInvalidStateTransitionError(any(), any())
+        verify(windowManager, never()).addView(any(), any())
+    }
+
+    @Test
     fun receivesNewStateFromCommandQueue_isLogged() {
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
@@ -575,6 +759,7 @@
 
     @Test
     fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() {
+        displayReceiverTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
             routeInfo,
@@ -598,6 +783,7 @@
 
     @Test
     fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() {
+        displayThisDeviceTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
             routeInfo,
@@ -621,6 +807,7 @@
 
     @Test
     fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+        displayReceiverTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
             routeInfo,
@@ -660,6 +847,7 @@
 
     @Test
     fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+        displayThisDeviceTriggered()
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(
             StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
             routeInfo,
@@ -717,6 +905,26 @@
     private fun ChipStateSender.getExpectedStateText(): String? {
         return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context)
     }
+
+    // display receiver triggered state helper method to make sure we start from a valid state
+    // transition (FAR_FROM_RECEIVER -> TRANSFER_TO_RECEIVER_TRIGGERED).
+    private fun displayReceiverTriggered() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+            routeInfo,
+            null
+        )
+    }
+
+    // display this device triggered state helper method to make sure we start from a valid state
+    // transition (FAR_FROM_RECEIVER -> TRANSFER_TO_THIS_DEVICE_TRIGGERED).
+    private fun displayThisDeviceTriggered() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+            routeInfo,
+            null
+        )
+    }
 }
 
 private const val APP_NAME = "Fake app name"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 9758842..4a9c750 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -16,16 +16,21 @@
 package com.android.systemui.notetask
 
 import android.app.KeyguardManager
+import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.os.UserManager
 import android.test.suitebuilder.annotation.SmallTest
-import android.view.KeyEvent
 import androidx.test.runner.AndroidJUnit4
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import org.junit.Before
 import org.junit.Test
@@ -48,6 +53,7 @@
     private val notesIntent = Intent(NOTES_ACTION)
 
     @Mock lateinit var context: Context
+    @Mock lateinit var packageManager: PackageManager
     @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
     @Mock lateinit var bubbles: Bubbles
     @Mock lateinit var optionalBubbles: Optional<Bubbles>
@@ -60,6 +66,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        whenever(context.packageManager).thenReturn(packageManager)
         whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
         whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
         whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
@@ -78,89 +85,125 @@
         )
     }
 
+    // region showNoteTask
     @Test
-    fun handleSystemKey_keyguardIsLocked_shouldStartActivity() {
+    fun showNoteTask_keyguardIsLocked_shouldStartActivity() {
         whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
 
-        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
 
         verify(context).startActivity(notesIntent)
         verify(bubbles, never()).showAppBubble(notesIntent)
     }
 
     @Test
-    fun handleSystemKey_keyguardIsUnlocked_shouldStartBubbles() {
+    fun showNoteTask_keyguardIsUnlocked_shouldStartBubbles() {
         whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
 
-        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
 
         verify(bubbles).showAppBubble(notesIntent)
         verify(context, never()).startActivity(notesIntent)
     }
 
     @Test
-    fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
-        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
+    fun showNoteTask_isInMultiWindowMode_shouldStartActivity() {
+        whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
 
-        verify(context, never()).startActivity(notesIntent)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = true)
+
+        verify(context).startActivity(notesIntent)
         verify(bubbles, never()).showAppBubble(notesIntent)
     }
 
     @Test
-    fun handleSystemKey_bubblesIsNull_shouldDoNothing() {
+    fun showNoteTask_bubblesIsNull_shouldDoNothing() {
         whenever(optionalBubbles.orElse(null)).thenReturn(null)
 
-        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
 
         verify(context, never()).startActivity(notesIntent)
         verify(bubbles, never()).showAppBubble(notesIntent)
     }
 
     @Test
-    fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() {
+    fun showNoteTask_keyguardManagerIsNull_shouldDoNothing() {
         whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
 
-        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
 
         verify(context, never()).startActivity(notesIntent)
         verify(bubbles, never()).showAppBubble(notesIntent)
     }
 
     @Test
-    fun handleSystemKey_userManagerIsNull_shouldDoNothing() {
+    fun showNoteTask_userManagerIsNull_shouldDoNothing() {
         whenever(optionalUserManager.orElse(null)).thenReturn(null)
 
-        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
 
         verify(context, never()).startActivity(notesIntent)
         verify(bubbles, never()).showAppBubble(notesIntent)
     }
 
     @Test
-    fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() {
+    fun showNoteTask_intentResolverReturnsNull_shouldDoNothing() {
         whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
 
-        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
 
         verify(context, never()).startActivity(notesIntent)
         verify(bubbles, never()).showAppBubble(notesIntent)
     }
 
     @Test
-    fun handleSystemKey_flagDisabled_shouldDoNothing() {
-        createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+    fun showNoteTask_flagDisabled_shouldDoNothing() {
+        createNoteTaskController(isEnabled = false).showNoteTask()
 
         verify(context, never()).startActivity(notesIntent)
         verify(bubbles, never()).showAppBubble(notesIntent)
     }
 
     @Test
-    fun handleSystemKey_userIsLocked_shouldDoNothing() {
+    fun showNoteTask_userIsLocked_shouldDoNothing() {
         whenever(userManager.isUserUnlocked).thenReturn(false)
 
-        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+        createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
 
         verify(context, never()).startActivity(notesIntent)
         verify(bubbles, never()).showAppBubble(notesIntent)
     }
+    // endregion
+
+    // region setNoteTaskShortcutEnabled
+    @Test
+    fun setNoteTaskShortcutEnabled_setTrue() {
+        createNoteTaskController().setNoteTaskShortcutEnabled(value = true)
+
+        val argument = argumentCaptor<ComponentName>()
+        verify(context.packageManager)
+            .setComponentEnabledSetting(
+                argument.capture(),
+                eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+                eq(PackageManager.DONT_KILL_APP),
+            )
+        val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
+        assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
+    }
+
+    @Test
+    fun setNoteTaskShortcutEnabled_setFalse() {
+        createNoteTaskController().setNoteTaskShortcutEnabled(value = false)
+
+        val argument = argumentCaptor<ComponentName>()
+        verify(context.packageManager)
+            .setComponentEnabledSetting(
+                argument.capture(),
+                eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+                eq(PackageManager.DONT_KILL_APP),
+            )
+        val expected = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
+        assertThat(argument.value.flattenToString()).isEqualTo(expected.flattenToString())
+    }
+    // endregion
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 334089c..538131a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -16,10 +16,10 @@
 package com.android.systemui.notetask
 
 import android.test.suitebuilder.annotation.SmallTest
+import android.view.KeyEvent
 import androidx.test.runner.AndroidJUnit4
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.wm.shell.bubbles.Bubbles
 import java.util.Optional
@@ -45,6 +45,7 @@
     @Mock lateinit var commandQueue: CommandQueue
     @Mock lateinit var bubbles: Bubbles
     @Mock lateinit var optionalBubbles: Optional<Bubbles>
+    @Mock lateinit var noteTaskController: NoteTaskController
 
     @Before
     fun setUp() {
@@ -57,12 +58,13 @@
     private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
         return NoteTaskInitializer(
             optionalBubbles = optionalBubbles,
-            lazyNoteTaskController = mock(),
+            noteTaskController = noteTaskController,
             commandQueue = commandQueue,
             isEnabled = isEnabled,
         )
     }
 
+    // region initializer
     @Test
     fun initialize_shouldAddCallbacks() {
         createNoteTaskInitializer().initialize()
@@ -85,4 +87,35 @@
 
         verify(commandQueue, never()).addCallback(any())
     }
+
+    @Test
+    fun initialize_flagEnabled_shouldEnableShortcut() {
+        createNoteTaskInitializer().initialize()
+
+        verify(noteTaskController).setNoteTaskShortcutEnabled(true)
+    }
+
+    @Test
+    fun initialize_flagDisabled_shouldDisableShortcut() {
+        createNoteTaskInitializer(isEnabled = false).initialize()
+
+        verify(noteTaskController).setNoteTaskShortcutEnabled(false)
+    }
+    // endregion
+
+    // region handleSystemKey
+    @Test
+    fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
+        createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(noteTaskController).showNoteTask()
+    }
+
+    @Test
+    fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
+        createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
+
+        verify(noteTaskController, never()).showNoteTask()
+    }
+    // endregion
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
index 5e9c1aa..906c20b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
@@ -703,28 +703,32 @@
     public void testParentalControls() {
         // Make sure the security footer is visible, so that the images are updated.
         when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true);
-
         when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
 
+        // We use the default icon when there is no admin icon.
+        when(mSecurityController.getIcon(any())).thenReturn(null);
+        mFooter.refreshState();
+        TestableLooper.get(this).processAllMessages();
+        assertEquals(mContext.getString(R.string.quick_settings_disclosure_parental_controls),
+                mFooterText.getText());
+        assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
+
         Drawable testDrawable = new VectorDrawable();
         when(mSecurityController.getIcon(any())).thenReturn(testDrawable);
         assertNotNull(mSecurityController.getIcon(null));
 
         mFooter.refreshState();
-
         TestableLooper.get(this).processAllMessages();
 
         assertEquals(mContext.getString(R.string.quick_settings_disclosure_parental_controls),
                 mFooterText.getText());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
-
         assertEquals(testDrawable, mPrimaryFooterIcon.getDrawable());
 
         // Ensure the primary icon is back to default after parental controls are gone
         when(mSecurityController.isParentalControlsEnabled()).thenReturn(false);
         mFooter.refreshState();
         TestableLooper.get(this).processAllMessages();
-
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
index 9c36be6..88651c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -23,9 +23,11 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Expect
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,6 +39,9 @@
     private lateinit var qsConstraint: ConstraintSet
     private lateinit var largeScreenConstraint: ConstraintSet
 
+    @get:Rule
+    val expect: Expect = Expect.create()
+
     @Before
     fun setUp() {
         qqsConstraint = ConstraintSet().apply {
@@ -344,6 +349,32 @@
     }
 
     @Test
+    fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() {
+        val views = mapOf(
+                R.id.clock to "clock",
+                R.id.date to "date",
+                R.id.statusIcons to "icons",
+                R.id.privacy_container to "privacy",
+                R.id.carrier_group to "carriers",
+                R.id.batteryRemainingIcon to "battery",
+        )
+        views.forEach { (id, name) ->
+            expect.withMessage("$name changes height")
+                    .that(qqsConstraint.getConstraint(id).layout.mHeight.fromConstraint())
+                    .isEqualTo(qsConstraint.getConstraint(id).layout.mHeight.fromConstraint())
+            expect.withMessage("$name changes width")
+                    .that(qqsConstraint.getConstraint(id).layout.mWidth.fromConstraint())
+                    .isEqualTo(qsConstraint.getConstraint(id).layout.mWidth.fromConstraint())
+        }
+    }
+
+    private fun Int.fromConstraint() = when (this) {
+        -1 -> "MATCH_PARENT"
+        -2 -> "WRAP_CONTENT"
+        else -> toString()
+    }
+
+    @Test
     fun testEmptyCutoutDateIconsAreConstrainedWidth() {
         CombinedShadeHeadersConstraintManagerImpl.emptyCutoutConstraints()()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index 858d0e7..1d30ad9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -46,7 +46,6 @@
 import com.android.systemui.qs.carrier.QSCarrierGroupController
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.HEADER_TRANSITION_ID
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
@@ -77,6 +76,7 @@
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
@@ -212,20 +212,6 @@
     }
 
     @Test
-    fun testCorrectConstraints() {
-        val captor = ArgumentCaptor.forClass(XmlResourceParser::class.java)
-
-        verify(qqsConstraints).load(eq(context), capture(captor))
-        assertThat(captor.value.getResId()).isEqualTo(R.xml.qqs_header)
-
-        verify(qsConstraints).load(eq(context), capture(captor))
-        assertThat(captor.value.getResId()).isEqualTo(R.xml.qs_header)
-
-        verify(largeScreenConstraints).load(eq(context), capture(captor))
-        assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header)
-    }
-
-    @Test
     fun testControllersCreatedAndInitialized() {
         verify(variableDateViewController).init()
 
@@ -287,16 +273,6 @@
     }
 
     @Test
-    fun testLargeScreenActive_true() {
-        controller.largeScreenActive = false // Make sure there's a change
-        clearInvocations(view)
-
-        controller.largeScreenActive = true
-
-        verify(view).setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
-    }
-
-    @Test
     fun testLargeScreenActive_false() {
         controller.largeScreenActive = true // Make sure there's a change
         clearInvocations(view)
@@ -696,6 +672,25 @@
         verify(clock).pivotY = height.toFloat() / 2
     }
 
+    @Test
+    fun onDensityOrFontScaleChanged_reloadConstraints() {
+        // After density or font scale change, constraints need to be reloaded to reflect new
+        // dimensions.
+        reset(qqsConstraints)
+        reset(qsConstraints)
+        reset(largeScreenConstraints)
+
+        configurationController.notifyDensityOrFontScaleChanged()
+
+        val captor = ArgumentCaptor.forClass(XmlResourceParser::class.java)
+        verify(qqsConstraints).load(eq(context), capture(captor))
+        assertThat(captor.value.getResId()).isEqualTo(R.xml.qqs_header)
+        verify(qsConstraints).load(eq(context), capture(captor))
+        assertThat(captor.value.getResId()).isEqualTo(R.xml.qs_header)
+        verify(largeScreenConstraints).load(eq(context), capture(captor))
+        assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header)
+    }
+
     private fun View.executeLayoutChange(
             left: Int,
             top: Int,
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 17ba30a..56a840c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -43,6 +43,7 @@
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -530,6 +531,8 @@
                 .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
         verify(mNotificationStackScrollLayoutController)
                 .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
+        verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
+        reset(mKeyguardStatusViewController);
     }
 
     @After
@@ -609,7 +612,7 @@
 
     @Test
     public void getVerticalSpaceForLockscreenNotifications_useLockIconBottomPadding_returnsSpaceAvailable() {
-        setBottomPadding(/* stackScrollLayoutBottom= */ 100,
+        setBottomPadding(/* stackScrollLayoutBottom= */ 180,
                 /* lockIconPadding= */ 20,
                 /* indicationPadding= */ 0,
                 /* ambientPadding= */ 0);
@@ -620,7 +623,7 @@
 
     @Test
     public void getVerticalSpaceForLockscreenNotifications_useIndicationBottomPadding_returnsSpaceAvailable() {
-        setBottomPadding(/* stackScrollLayoutBottom= */ 100,
+        setBottomPadding(/* stackScrollLayoutBottom= */ 180,
                 /* lockIconPadding= */ 0,
                 /* indicationPadding= */ 30,
                 /* ambientPadding= */ 0);
@@ -631,7 +634,7 @@
 
     @Test
     public void getVerticalSpaceForLockscreenNotifications_useAmbientBottomPadding_returnsSpaceAvailable() {
-        setBottomPadding(/* stackScrollLayoutBottom= */ 100,
+        setBottomPadding(/* stackScrollLayoutBottom= */ 180,
                 /* lockIconPadding= */ 0,
                 /* indicationPadding= */ 0,
                 /* ambientPadding= */ 40);
@@ -954,7 +957,7 @@
     }
 
     @Test
-    public void testFinishInflate_userSwitcherDisabled_doNotInflateUserSwitchView() {
+    public void testFinishInflate_userSwitcherDisabled_doNotInflateUserSwitchView_initClock() {
         givenViewAttached();
         when(mResources.getBoolean(
                 com.android.internal.R.bool.config_keyguardUserSwitcher)).thenReturn(true);
@@ -965,6 +968,7 @@
         mNotificationPanelViewController.onFinishInflate();
 
         verify(mUserSwitcherStubView, never()).inflate();
+        verify(mKeyguardStatusViewController, times(3)).displayClock(LARGE, /* animate */ true);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
index faf4592..5431eba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
@@ -72,6 +72,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
@@ -245,6 +246,7 @@
                 mFakeExecutor,
                 mCallbackHandler,
                 mock(AccessPointControllerImpl.class),
+                mock(StatusBarPipelineFlags.class),
                 mock(DataUsageController.class),
                 mMockSubDefaults,
                 mMockProvisionController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
index ca75a40..9441d49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
@@ -49,6 +49,7 @@
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
@@ -150,6 +151,7 @@
                 mFakeExecutor,
                 mCallbackHandler,
                 mock(AccessPointControllerImpl.class),
+                mock(StatusBarPipelineFlags.class),
                 mock(DataUsageController.class),
                 mMockSubDefaults,
                 mock(DeviceProvisionedController.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
index 84c242c..4c1f0a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
@@ -44,6 +44,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
@@ -78,6 +79,7 @@
                 mFakeExecutor,
                 mCallbackHandler,
                 mock(AccessPointControllerImpl.class),
+                mock(StatusBarPipelineFlags.class),
                 mock(DataUsageController.class),
                 mMockSubDefaults,
                 mMockProvisionController,
@@ -115,6 +117,7 @@
                 mFakeExecutor,
                 mCallbackHandler,
                 mock(AccessPointControllerImpl.class),
+                mock(StatusBarPipelineFlags.class),
                 mock(DataUsageController.class),
                 mMockSubDefaults,
                 mMockProvisionController,
@@ -150,6 +153,7 @@
                 mFakeExecutor,
                 mCallbackHandler,
                 mock(AccessPointControllerImpl.class),
+                mock(StatusBarPipelineFlags.class),
                 mock(DataUsageController.class),
                 mMockSubDefaults,
                 mock(DeviceProvisionedController.class),
@@ -188,6 +192,7 @@
                 mFakeExecutor,
                 mCallbackHandler,
                 mock(AccessPointControllerImpl.class),
+                mock(StatusBarPipelineFlags.class),
                 mock(DataUsageController.class),
                 mMockSubDefaults,
                 mock(DeviceProvisionedController.class),
@@ -274,6 +279,7 @@
                 mFakeExecutor,
                 mCallbackHandler,
                 mock(AccessPointControllerImpl.class),
+                mock(StatusBarPipelineFlags.class),
                 mock(DataUsageController.class),
                 mMockSubDefaults,
                 mock(DeviceProvisionedController.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
new file mode 100644
index 0000000..89faa239
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
@@ -0,0 +1,164 @@
+package com.android.systemui.statusbar.notification
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class RoundableTest : SysuiTestCase() {
+    val targetView: View = mock()
+    val roundable = FakeRoundable(targetView)
+
+    @Test
+    fun defaultConfig_shouldNotHaveRoundedCorner() {
+        // the expected default value for the roundness is top = 0f, bottom = 0f
+        assertEquals(0f, roundable.roundableState.topRoundness)
+        assertEquals(0f, roundable.roundableState.bottomRoundness)
+        assertEquals(false, roundable.hasRoundedCorner())
+    }
+
+    @Test
+    fun applyRoundnessAndInvalidate_should_invalidate_targetView() {
+        roundable.applyRoundnessAndInvalidate()
+
+        verify(targetView, times(1)).invalidate()
+    }
+
+    @Test
+    fun requestTopRoundness_update_and_invalidate_targetView() {
+        roundable.requestTopRoundness(value = 1f, sourceType = SOURCE1)
+
+        assertEquals(1f, roundable.roundableState.topRoundness)
+        verify(targetView, times(1)).invalidate()
+    }
+
+    @Test
+    fun requestBottomRoundness_update_and_invalidate_targetView() {
+        roundable.requestBottomRoundness(value = 1f, sourceType = SOURCE1)
+
+        assertEquals(1f, roundable.roundableState.bottomRoundness)
+        verify(targetView, times(1)).invalidate()
+    }
+
+    @Test
+    fun requestRoundness_update_and_invalidate_targetView() {
+        roundable.requestRoundness(top = 1f, bottom = 1f, sourceType = SOURCE1)
+
+        assertEquals(1f, roundable.roundableState.topRoundness)
+        assertEquals(1f, roundable.roundableState.bottomRoundness)
+        verify(targetView, atLeastOnce()).invalidate()
+    }
+
+    @Test
+    fun requestRoundnessReset_update_and_invalidate_targetView() {
+        roundable.requestRoundness(1f, 1f, SOURCE1)
+        assertEquals(1f, roundable.roundableState.topRoundness)
+        assertEquals(1f, roundable.roundableState.bottomRoundness)
+
+        roundable.requestRoundnessReset(sourceType = SOURCE1)
+
+        assertEquals(0f, roundable.roundableState.topRoundness)
+        assertEquals(0f, roundable.roundableState.bottomRoundness)
+        verify(targetView, atLeastOnce()).invalidate()
+    }
+
+    @Test
+    fun hasRoundedCorner_return_true_ifRoundnessIsGreaterThenZero() {
+        roundable.requestRoundness(top = 1f, bottom = 1f, sourceType = SOURCE1)
+        assertEquals(true, roundable.hasRoundedCorner())
+
+        roundable.requestRoundness(top = 1f, bottom = 0f, sourceType = SOURCE1)
+        assertEquals(true, roundable.hasRoundedCorner())
+
+        roundable.requestRoundness(top = 0f, bottom = 1f, sourceType = SOURCE1)
+        assertEquals(true, roundable.hasRoundedCorner())
+
+        roundable.requestRoundness(top = 0f, bottom = 0f, sourceType = SOURCE1)
+        assertEquals(false, roundable.hasRoundedCorner())
+    }
+
+    @Test
+    fun roundness_take_maxValue_onMultipleSources_first_lower() {
+        roundable.requestRoundness(0.1f, 0.1f, SOURCE1)
+        assertEquals(0.1f, roundable.roundableState.topRoundness)
+        assertEquals(0.1f, roundable.roundableState.bottomRoundness)
+
+        roundable.requestRoundness(0.2f, 0.2f, SOURCE2)
+        // SOURCE1 has 0.1f - SOURCE2 has 0.2f
+        assertEquals(0.2f, roundable.roundableState.topRoundness)
+        assertEquals(0.2f, roundable.roundableState.bottomRoundness)
+    }
+
+    @Test
+    fun roundness_take_maxValue_onMultipleSources_first_higher() {
+        roundable.requestRoundness(0.5f, 0.5f, SOURCE1)
+        assertEquals(0.5f, roundable.roundableState.topRoundness)
+        assertEquals(0.5f, roundable.roundableState.bottomRoundness)
+
+        roundable.requestRoundness(0.1f, 0.1f, SOURCE2)
+        // SOURCE1 has 0.5f - SOURCE2 has 0.1f
+        assertEquals(0.5f, roundable.roundableState.topRoundness)
+        assertEquals(0.5f, roundable.roundableState.bottomRoundness)
+    }
+
+    @Test
+    fun roundness_take_maxValue_onMultipleSources_first_higher_second_step() {
+        roundable.requestRoundness(0.1f, 0.1f, SOURCE1)
+        assertEquals(0.1f, roundable.roundableState.topRoundness)
+        assertEquals(0.1f, roundable.roundableState.bottomRoundness)
+
+        roundable.requestRoundness(0.2f, 0.2f, SOURCE2)
+        // SOURCE1 has 0.1f - SOURCE2 has 0.2f
+        assertEquals(0.2f, roundable.roundableState.topRoundness)
+        assertEquals(0.2f, roundable.roundableState.bottomRoundness)
+
+        roundable.requestRoundness(0.3f, 0.3f, SOURCE1)
+        // SOURCE1 has 0.3f - SOURCE2 has 0.2f
+        assertEquals(0.3f, roundable.roundableState.topRoundness)
+        assertEquals(0.3f, roundable.roundableState.bottomRoundness)
+    }
+
+    @Test
+    fun roundness_take_maxValue_onMultipleSources_first_lower_second_step() {
+        roundable.requestRoundness(0.5f, 0.5f, SOURCE1)
+        assertEquals(0.5f, roundable.roundableState.topRoundness)
+        assertEquals(0.5f, roundable.roundableState.bottomRoundness)
+
+        roundable.requestRoundness(0.2f, 0.2f, SOURCE2)
+        // SOURCE1 has 0.5f - SOURCE2 has 0.2f
+        assertEquals(0.5f, roundable.roundableState.topRoundness)
+        assertEquals(0.5f, roundable.roundableState.bottomRoundness)
+
+        roundable.requestRoundness(0.1f, 0.1f, SOURCE1)
+        // SOURCE1 has 0.1f - SOURCE2 has 0.2f
+        assertEquals(0.2f, roundable.roundableState.topRoundness)
+        assertEquals(0.2f, roundable.roundableState.bottomRoundness)
+    }
+
+    class FakeRoundable(
+        targetView: View,
+        radius: Float = MAX_RADIUS,
+    ) : Roundable {
+        override val roundableState =
+            RoundableState(
+                targetView = targetView,
+                roundable = this,
+                maxRadius = radius,
+            )
+    }
+
+    companion object {
+        private const val MAX_RADIUS = 10f
+        private val SOURCE1 = SourceType.from("Source1")
+        private val SOURCE2 = SourceType.from("Source2")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 088d165..59d4720 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -213,8 +213,7 @@
             SourceType sourceType
     ) throws Exception {
         ExpandableNotificationRow row = createRow();
-        row.requestTopRoundness(topRoundness, false, sourceType);
-        row.requestBottomRoundness(bottomRoundness, /*animate = */ false, sourceType);
+        row.requestRoundness(topRoundness, bottomRoundness, sourceType, /*animate = */ false);
         assertEquals(topRoundness, row.getTopRoundness(), /* delta = */ 0f);
         assertEquals(bottomRoundness, row.getBottomRoundness(), /* delta = */ 0f);
         return row;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 438b528..fd1944e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -25,7 +25,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.notification.SourceType;
+import com.android.systemui.statusbar.notification.LegacySourceType;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
 
@@ -158,7 +158,7 @@
         ExpandableNotificationRow row = mNotificationTestHelper.createRowWithRoundness(
                 /* topRoundness = */ 1f,
                 /* bottomRoundness = */ 1f,
-                /* sourceType = */ SourceType.OnScroll);
+                /* sourceType = */ LegacySourceType.OnScroll);
 
         mChildrenContainer.addNotification(row, 0);
 
@@ -171,11 +171,11 @@
         ExpandableNotificationRow row1 = mNotificationTestHelper.createRowWithRoundness(
                 /* topRoundness = */ 1f,
                 /* bottomRoundness = */ 1f,
-                /* sourceType = */ SourceType.DefaultValue);
+                /* sourceType = */ LegacySourceType.DefaultValue);
         ExpandableNotificationRow row2 = mNotificationTestHelper.createRowWithRoundness(
                 /* topRoundness = */ 1f,
                 /* bottomRoundness = */ 1f,
-                /* sourceType = */ SourceType.OnDismissAnimation);
+                /* sourceType = */ LegacySourceType.OnDismissAnimation);
 
         mChildrenContainer.addNotification(row1, 0);
         mChildrenContainer.addNotification(row2, 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index 8c8b644..bd0a556 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
@@ -35,6 +35,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger;
@@ -73,7 +74,8 @@
         mRoundnessManager = new NotificationRoundnessManager(
                 new NotificationSectionsFeatureManager(new DeviceConfigProxy(), mContext),
                 mLogger,
-                mock(DumpManager.class));
+                mock(DumpManager.class),
+                mock(FeatureFlags.class));
         allowTestableLooperAsMainThread();
         NotificationTestHelper testHelper = new NotificationTestHelper(
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index ecc0224..30da08e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -30,6 +30,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
@@ -59,10 +60,12 @@
     @Mock private KeyguardMediaController mKeyguardMediaController;
     @Mock private NotificationSectionsFeatureManager mSectionsFeatureManager;
     @Mock private MediaContainerController mMediaContainerController;
+    @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
     @Mock private SectionHeaderController mIncomingHeaderController;
     @Mock private SectionHeaderController mPeopleHeaderController;
     @Mock private SectionHeaderController mAlertingHeaderController;
     @Mock private SectionHeaderController mSilentHeaderController;
+    @Mock private FeatureFlags mFeatureFlag;
 
     private NotificationSectionsManager mSectionsManager;
 
@@ -89,10 +92,12 @@
                         mKeyguardMediaController,
                         mSectionsFeatureManager,
                         mMediaContainerController,
+                        mNotificationRoundnessManager,
                         mIncomingHeaderController,
                         mPeopleHeaderController,
                         mAlertingHeaderController,
-                        mSilentHeaderController
+                        mSilentHeaderController,
+                        mFeatureFlag
                 );
         // Required in order for the header inflation to work properly
         when(mNssl.generateLayoutParams(any(AttributeSet.class)))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index bda2336..9d759c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -9,7 +9,7 @@
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.SourceType
+import com.android.systemui.statusbar.notification.LegacySourceType
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
@@ -314,9 +314,9 @@
         val row: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
                 /* topRoundness = */ 1f,
                 /* bottomRoundness = */ 1f,
-                /* sourceType = */ SourceType.OnScroll)
+                /* sourceType = */ LegacySourceType.OnScroll)
 
-        NotificationShelf.resetOnScrollRoundness(row)
+        NotificationShelf.resetLegacyOnScrollRoundness(row)
 
         assertEquals(0f, row.topRoundness)
         assertEquals(0f, row.bottomRoundness)
@@ -327,14 +327,14 @@
         val row1: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
                 /* topRoundness = */ 1f,
                 /* bottomRoundness = */ 1f,
-                /* sourceType = */ SourceType.DefaultValue)
+                /* sourceType = */ LegacySourceType.DefaultValue)
         val row2: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
                 /* topRoundness = */ 1f,
                 /* bottomRoundness = */ 1f,
-                /* sourceType = */ SourceType.OnDismissAnimation)
+                /* sourceType = */ LegacySourceType.OnDismissAnimation)
 
-        NotificationShelf.resetOnScrollRoundness(row1)
-        NotificationShelf.resetOnScrollRoundness(row2)
+        NotificationShelf.resetLegacyOnScrollRoundness(row1)
+        NotificationShelf.resetLegacyOnScrollRoundness(row2)
 
         assertEquals(1f, row1.topRoundness)
         assertEquals(1f, row1.bottomRoundness)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 4ea1c71..680a323 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -74,6 +74,7 @@
     private NotificationSwipeHelper mSwipeHelper;
     private NotificationSwipeHelper.NotificationCallback mCallback;
     private NotificationMenuRowPlugin.OnMenuEventListener mListener;
+    private NotificationRoundnessManager mNotificationRoundnessManager;
     private View mView;
     private MotionEvent mEvent;
     private NotificationMenuRowPlugin mMenuRow;
@@ -92,10 +93,17 @@
     public void setUp() throws Exception {
         mCallback = mock(NotificationSwipeHelper.NotificationCallback.class);
         mListener = mock(NotificationMenuRowPlugin.OnMenuEventListener.class);
+        mNotificationRoundnessManager = mock(NotificationRoundnessManager.class);
         mFeatureFlags = mock(FeatureFlags.class);
         mSwipeHelper = spy(new NotificationSwipeHelper(
-                mContext.getResources(), ViewConfiguration.get(mContext),
-                new FalsingManagerFake(), mFeatureFlags, SwipeHelper.X, mCallback, mListener));
+                mContext.getResources(),
+                ViewConfiguration.get(mContext),
+                new FalsingManagerFake(),
+                mFeatureFlags,
+                SwipeHelper.X,
+                mCallback,
+                mListener,
+                mNotificationRoundnessManager));
         mView = mock(View.class);
         mEvent = mock(MotionEvent.class);
         mMenuRow = mock(NotificationMenuRowPlugin.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
index a2e9230..81a3f12 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
@@ -35,7 +35,7 @@
     ) =
         NotificationTargetsHelper(
             FakeFeatureFlags().apply {
-                set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner)
+                set(Flags.USE_ROUNDNESS_SOURCETYPES, notificationGroupCorner)
             }
         )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
index 038af8f..6155e3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.doAnswer
@@ -295,6 +296,7 @@
     }
 
     @Test
+    @Ignore("b/261408895")
     fun equivalentConfigObject_listenerNotNotified() {
         val config = mContext.resources.configuration
         val listener = createAndAddListener()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
index 103b7b42..9727b6c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
@@ -32,6 +32,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
@@ -40,6 +41,7 @@
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.policy.Clock;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -71,6 +73,8 @@
     private NotificationWakeUpCoordinator mWakeUpCoordinator;
     private KeyguardStateController mKeyguardStateController;
     private CommandQueue mCommandQueue;
+    private NotificationRoundnessManager mNotificationRoundnessManager;
+    private FeatureFlags mFeatureFlag;
 
     @Before
     public void setUp() throws Exception {
@@ -89,6 +93,8 @@
         mWakeUpCoordinator = mock(NotificationWakeUpCoordinator.class);
         mKeyguardStateController = mock(KeyguardStateController.class);
         mCommandQueue = mock(CommandQueue.class);
+        mNotificationRoundnessManager = mock(NotificationRoundnessManager.class);
+        mFeatureFlag = mock(FeatureFlags.class);
         mHeadsUpAppearanceController = new HeadsUpAppearanceController(
                 mock(NotificationIconAreaController.class),
                 mHeadsUpManager,
@@ -100,6 +106,8 @@
                 mCommandQueue,
                 mStackScrollerController,
                 mPanelView,
+                mNotificationRoundnessManager,
+                mFeatureFlag,
                 mHeadsUpStatusBarView,
                 new Clock(mContext, null),
                 Optional.of(mOperatorNameView));
@@ -182,6 +190,8 @@
                 mCommandQueue,
                 mStackScrollerController,
                 mPanelView,
+                mNotificationRoundnessManager,
+                mFeatureFlag,
                 mHeadsUpStatusBarView,
                 new Clock(mContext, null),
                 Optional.empty());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt
new file mode 100644
index 0000000..7eba3b46
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.content.pm.UserInfo
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import junit.framework.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class ManagedProfileControllerImplTest : SysuiTestCase() {
+
+    private val mainExecutor: FakeExecutor = FakeExecutor(FakeSystemClock())
+
+    private lateinit var controller: ManagedProfileControllerImpl
+
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var userManager: UserManager
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        controller = ManagedProfileControllerImpl(context, mainExecutor, userTracker, userManager)
+    }
+
+    @Test
+    fun hasWorkingProfile_isWorkModeEnabled_returnsTrue() {
+        `when`(userTracker.userId).thenReturn(1)
+        setupWorkingProfile(1)
+
+        Assert.assertEquals(true, controller.hasActiveProfile())
+    }
+
+    @Test
+    fun noWorkingProfile_isWorkModeEnabled_returnsFalse() {
+        `when`(userTracker.userId).thenReturn(1)
+
+        Assert.assertEquals(false, controller.hasActiveProfile())
+    }
+
+    @Test
+    fun listeningUserChanges_isWorkModeEnabled_returnsTrue() {
+        `when`(userTracker.userId).thenReturn(1)
+        controller.addCallback(TestCallback)
+        `when`(userTracker.userId).thenReturn(2)
+        setupWorkingProfile(2)
+
+        Assert.assertEquals(true, controller.hasActiveProfile())
+    }
+
+    private fun setupWorkingProfile(userId: Int) {
+        `when`(userManager.getEnabledProfiles(userId))
+            .thenReturn(
+                listOf(UserInfo(userId, "test_user", "", 0, UserManager.USER_TYPE_PROFILE_MANAGED))
+            )
+    }
+
+    private object TestCallback : ManagedProfileController.Callback {
+
+        override fun onManagedProfileChanged() = Unit
+
+        override fun onManagedProfileRemoved() = Unit
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
new file mode 100644
index 0000000..96a280a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.net.ConnectivityManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * The switcher acts as a dispatcher to either the `prod` or `demo` versions of the repository
+ * interface it's switching on. These tests just need to verify that the entire interface properly
+ * switches over when the value of `demoMode` changes
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MobileRepositorySwitcherTest : SysuiTestCase() {
+    private lateinit var underTest: MobileRepositorySwitcher
+    private lateinit var realRepo: MobileConnectionsRepositoryImpl
+    private lateinit var demoRepo: DemoMobileConnectionsRepository
+    private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+
+    @Mock private lateinit var connectivityManager: ConnectivityManager
+    @Mock private lateinit var subscriptionManager: SubscriptionManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var demoModeController: DemoModeController
+
+    private val globalSettings = FakeSettings()
+    private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        // Never start in demo mode
+        whenever(demoModeController.isInDemoMode).thenReturn(false)
+
+        mockDataSource =
+            mock<DemoModeMobileConnectionDataSource>().also {
+                whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
+            }
+
+        realRepo =
+            MobileConnectionsRepositoryImpl(
+                connectivityManager,
+                subscriptionManager,
+                telephonyManager,
+                logger,
+                fakeBroadcastDispatcher,
+                globalSettings,
+                context,
+                IMMEDIATE,
+                scope,
+                mock(),
+            )
+
+        demoRepo =
+            DemoMobileConnectionsRepository(
+                dataSource = mockDataSource,
+                scope = scope,
+                context = context,
+            )
+
+        underTest =
+            MobileRepositorySwitcher(
+                scope = scope,
+                realRepository = realRepo,
+                demoMobileConnectionsRepository = demoRepo,
+                demoModeController = demoModeController,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun `active repo matches demo mode setting`() =
+        runBlocking(IMMEDIATE) {
+            whenever(demoModeController.isInDemoMode).thenReturn(false)
+
+            var latest: MobileConnectionsRepository? = null
+            val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(realRepo)
+
+            startDemoMode()
+
+            assertThat(latest).isEqualTo(demoRepo)
+
+            finishDemoMode()
+
+            assertThat(latest).isEqualTo(realRepo)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `subscription list updates when demo mode changes`() =
+        runBlocking(IMMEDIATE) {
+            whenever(demoModeController.isInDemoMode).thenReturn(false)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            // The real subscriptions has 2 subs
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            // Demo mode turns on, and we should see only the demo subscriptions
+            startDemoMode()
+            fakeNetworkEventsFlow.value = validMobileEvent(subId = 3)
+
+            // Demo mobile connections repository makes arbitrarily-formed subscription info
+            // objects, so just validate the data we care about
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(3)
+
+            finishDemoMode()
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            job.cancel()
+        }
+
+    private fun startDemoMode() {
+        whenever(demoModeController.isInDemoMode).thenReturn(true)
+        getDemoModeCallback().onDemoModeStarted()
+    }
+
+    private fun finishDemoMode() {
+        whenever(demoModeController.isInDemoMode).thenReturn(false)
+        getDemoModeCallback().onDemoModeFinished()
+    }
+
+    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
+        val callbackCaptor =
+            kotlinArgumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+        verify(subscriptionManager)
+            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
+        return callbackCaptor.value
+    }
+
+    private fun getDemoModeCallback(): DemoMode {
+        val captor = kotlinArgumentCaptor<DemoMode>()
+        verify(demoModeController).addCallback(captor.capture())
+        return captor.value
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
new file mode 100644
index 0000000..bf5ecd8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
+
+import android.telephony.Annotation
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+/**
+ * Parameterized test for all of the common values of [FakeNetworkEventModel]. This test simply
+ * verifies that passing the given model to [DemoMobileConnectionsRepository] results in the correct
+ * flows emitting from the given connection.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(Parameterized::class)
+internal class DemoMobileConnectionParameterizedTest(private val testCase: TestCase) :
+    SysuiTestCase() {
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+
+    private lateinit var connectionsRepo: DemoMobileConnectionsRepository
+    private lateinit var underTest: DemoMobileConnectionRepository
+    private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+
+    @Before
+    fun setUp() {
+        // The data source only provides one API, so we can mock it with a flow here for convenience
+        mockDataSource =
+            mock<DemoModeMobileConnectionDataSource>().also {
+                whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
+            }
+
+        connectionsRepo =
+            DemoMobileConnectionsRepository(
+                dataSource = mockDataSource,
+                scope = testScope.backgroundScope,
+                context = context,
+            )
+
+        connectionsRepo.startProcessingCommands()
+    }
+
+    @After
+    fun tearDown() {
+        testScope.cancel()
+    }
+
+    @Test
+    fun demoNetworkData() =
+        testScope.runTest {
+            val networkModel =
+                FakeNetworkEventModel.Mobile(
+                    level = testCase.level,
+                    dataType = testCase.dataType,
+                    subId = testCase.subId,
+                    carrierId = testCase.carrierId,
+                    inflateStrength = testCase.inflateStrength,
+                    activity = testCase.activity,
+                    carrierNetworkChange = testCase.carrierNetworkChange,
+                )
+
+            fakeNetworkEventFlow.value = networkModel
+            underTest = connectionsRepo.getRepoForSubId(subId)
+
+            assertConnection(underTest, networkModel)
+        }
+
+    private fun assertConnection(
+        conn: DemoMobileConnectionRepository,
+        model: FakeNetworkEventModel
+    ) {
+        when (model) {
+            is FakeNetworkEventModel.Mobile -> {
+                val subscriptionModel: MobileSubscriptionModel = conn.subscriptionModelFlow.value
+                assertThat(conn.subId).isEqualTo(model.subId)
+                assertThat(subscriptionModel.cdmaLevel).isEqualTo(model.level)
+                assertThat(subscriptionModel.primaryLevel).isEqualTo(model.level)
+                assertThat(subscriptionModel.dataActivityDirection).isEqualTo(model.activity)
+                assertThat(subscriptionModel.carrierNetworkChangeActive)
+                    .isEqualTo(model.carrierNetworkChange)
+
+                // TODO(b/261029387): check these once we start handling them
+                assertThat(subscriptionModel.isEmergencyOnly).isFalse()
+                assertThat(subscriptionModel.isGsm).isFalse()
+                assertThat(subscriptionModel.dataConnectionState)
+                    .isEqualTo(DataConnectionState.Connected)
+            }
+            // MobileDisabled isn't combinatorial in nature, and is tested in
+            // DemoMobileConnectionsRepositoryTest.kt
+            else -> {}
+        }
+    }
+
+    /** Matches [FakeNetworkEventModel] */
+    internal data class TestCase(
+        val level: Int,
+        val dataType: SignalIcon.MobileIconGroup,
+        val subId: Int,
+        val carrierId: Int,
+        val inflateStrength: Boolean,
+        @Annotation.DataActivityType val activity: Int,
+        val carrierNetworkChange: Boolean,
+    ) {
+        override fun toString(): String {
+            return "INPUT(level=$level, " +
+                "dataType=${dataType.name}, " +
+                "subId=$subId, " +
+                "carrierId=$carrierId, " +
+                "inflateStrength=$inflateStrength, " +
+                "activity=$activity, " +
+                "carrierNetworkChange=$carrierNetworkChange)"
+        }
+
+        // Convenience for iterating test data and creating new cases
+        fun modifiedBy(
+            level: Int? = null,
+            dataType: SignalIcon.MobileIconGroup? = null,
+            subId: Int? = null,
+            carrierId: Int? = null,
+            inflateStrength: Boolean? = null,
+            @Annotation.DataActivityType activity: Int? = null,
+            carrierNetworkChange: Boolean? = null,
+        ): TestCase =
+            TestCase(
+                level = level ?: this.level,
+                dataType = dataType ?: this.dataType,
+                subId = subId ?: this.subId,
+                carrierId = carrierId ?: this.carrierId,
+                inflateStrength = inflateStrength ?: this.inflateStrength,
+                activity = activity ?: this.activity,
+                carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange
+            )
+    }
+
+    companion object {
+        private val subId = 1
+
+        private val booleanList = listOf(true, false)
+        private val levels = listOf(0, 1, 2, 3)
+        private val dataTypes =
+            listOf(
+                TelephonyIcons.THREE_G,
+                TelephonyIcons.LTE,
+                TelephonyIcons.FOUR_G,
+                TelephonyIcons.NR_5G,
+                TelephonyIcons.NR_5G_PLUS,
+            )
+        private val carrierIds = listOf(1, 10, 100)
+        private val inflateStrength = booleanList
+        private val activity =
+            listOf(
+                TelephonyManager.DATA_ACTIVITY_NONE,
+                TelephonyManager.DATA_ACTIVITY_IN,
+                TelephonyManager.DATA_ACTIVITY_OUT,
+                TelephonyManager.DATA_ACTIVITY_INOUT
+            )
+        private val carrierNetworkChange = booleanList
+
+        @Parameters(name = "{0}") @JvmStatic fun data() = testData()
+
+        /**
+         * Generate some test data. For the sake of convenience, we'll parameterize only non-null
+         * network event data. So given the lists of test data:
+         * ```
+         *    list1 = [1, 2, 3]
+         *    list2 = [false, true]
+         *    list3 = [a, b, c]
+         * ```
+         * We'll generate test cases for:
+         *
+         * Test (1, false, a) Test (2, false, a) Test (3, false, a) Test (1, true, a) Test (1,
+         * false, b) Test (1, false, c)
+         *
+         * NOTE: this is not a combinatorial product of all of the possible sets of parameters.
+         * Since this test is built to exercise demo mode, the general approach is to define a
+         * fully-formed "base case", and from there to make sure to use every valid parameter once,
+         * by defining the rest of the test cases against the base case. Specific use-cases can be
+         * added to the non-parameterized test, or manually below the generated test cases.
+         */
+        private fun testData(): List<TestCase> {
+            val testSet = mutableSetOf<TestCase>()
+
+            val baseCase =
+                TestCase(
+                    levels.first(),
+                    dataTypes.first(),
+                    subId,
+                    carrierIds.first(),
+                    inflateStrength.first(),
+                    activity.first(),
+                    carrierNetworkChange.first()
+                )
+
+            val tail =
+                sequenceOf(
+                        levels.map { baseCase.modifiedBy(level = it) },
+                        dataTypes.map { baseCase.modifiedBy(dataType = it) },
+                        carrierIds.map { baseCase.modifiedBy(carrierId = it) },
+                        inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
+                        activity.map { baseCase.modifiedBy(activity = it) },
+                        carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
+                    )
+                    .flatten()
+
+            testSet.add(baseCase)
+            tail.toCollection(testSet)
+
+            return testSet.toList()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
new file mode 100644
index 0000000..a8f6993
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
+
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
+import androidx.test.filters.SmallTest
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons.THREE_G
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class DemoMobileConnectionsRepositoryTest : SysuiTestCase() {
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+
+    private lateinit var underTest: DemoMobileConnectionsRepository
+    private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+
+    @Before
+    fun setUp() {
+        // The data source only provides one API, so we can mock it with a flow here for convenience
+        mockDataSource =
+            mock<DemoModeMobileConnectionDataSource>().also {
+                whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
+            }
+
+        underTest =
+            DemoMobileConnectionsRepository(
+                dataSource = mockDataSource,
+                scope = testScope.backgroundScope,
+                context = context,
+            )
+
+        underTest.startProcessingCommands()
+    }
+
+    @Test
+    fun `network event - create new subscription`() =
+        testScope.runTest {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEmpty()
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network event - reuses subscription when same Id`() =
+        testScope.runTest {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEmpty()
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(1)
+
+            // Second network event comes in with the same subId, does not create a new subscription
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 2)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `multiple subscriptions`() =
+        testScope.runTest {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
+
+            assertThat(latest).hasSize(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `mobile disabled event - disables connection - subId specified - single conn`() =
+        testScope.runTest {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+
+            fakeNetworkEventFlow.value = MobileDisabled(subId = 1)
+
+            assertThat(latest).hasSize(0)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `mobile disabled event - disables connection - subId not specified - single conn`() =
+        testScope.runTest {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+
+            fakeNetworkEventFlow.value = MobileDisabled(subId = null)
+
+            assertThat(latest).hasSize(0)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `mobile disabled event - disables connection - subId specified - multiple conn`() =
+        testScope.runTest {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
+
+            fakeNetworkEventFlow.value = MobileDisabled(subId = 2)
+
+            assertThat(latest).hasSize(1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `mobile disabled event - subId not specified - multiple conn - ignores command`() =
+        testScope.runTest {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
+
+            fakeNetworkEventFlow.value = MobileDisabled(subId = null)
+
+            assertThat(latest).hasSize(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `demo connection - single subscription`() =
+        testScope.runTest {
+            var currentEvent: FakeNetworkEventModel = validMobileEvent(subId = 1)
+            var connections: List<DemoMobileConnectionRepository>? = null
+            val job =
+                underTest.subscriptionsFlow
+                    .onEach { infos ->
+                        connections =
+                            infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+                    }
+                    .launchIn(this)
+
+            fakeNetworkEventFlow.value = currentEvent
+
+            assertThat(connections).hasSize(1)
+            val connection1 = connections!![0]
+
+            assertConnection(connection1, currentEvent)
+
+            // Exercise the whole api
+
+            currentEvent = validMobileEvent(subId = 1, level = 2)
+            fakeNetworkEventFlow.value = currentEvent
+            assertConnection(connection1, currentEvent)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `demo connection - two connections - update second - no affect on first`() =
+        testScope.runTest {
+            var currentEvent1 = validMobileEvent(subId = 1)
+            var connection1: DemoMobileConnectionRepository? = null
+            var currentEvent2 = validMobileEvent(subId = 2)
+            var connection2: DemoMobileConnectionRepository? = null
+            var connections: List<DemoMobileConnectionRepository>? = null
+            val job =
+                underTest.subscriptionsFlow
+                    .onEach { infos ->
+                        connections =
+                            infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+                    }
+                    .launchIn(this)
+
+            fakeNetworkEventFlow.value = currentEvent1
+            fakeNetworkEventFlow.value = currentEvent2
+            assertThat(connections).hasSize(2)
+            connections!!.forEach {
+                if (it.subId == 1) {
+                    connection1 = it
+                } else if (it.subId == 2) {
+                    connection2 = it
+                } else {
+                    Assert.fail("Unexpected subscription")
+                }
+            }
+
+            assertConnection(connection1!!, currentEvent1)
+            assertConnection(connection2!!, currentEvent2)
+
+            // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
+            currentEvent2 = validMobileEvent(subId = 2, activity = DATA_ACTIVITY_INOUT)
+            fakeNetworkEventFlow.value = currentEvent2
+            assertConnection(connection1!!, currentEvent1)
+            assertConnection(connection2!!, currentEvent2)
+
+            // and vice versa
+            currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
+            fakeNetworkEventFlow.value = currentEvent1
+            assertConnection(connection1!!, currentEvent1)
+            assertConnection(connection2!!, currentEvent2)
+
+            job.cancel()
+        }
+
+    private fun assertConnection(
+        conn: DemoMobileConnectionRepository,
+        model: FakeNetworkEventModel
+    ) {
+        when (model) {
+            is FakeNetworkEventModel.Mobile -> {
+                val subscriptionModel: MobileSubscriptionModel = conn.subscriptionModelFlow.value
+                assertThat(conn.subId).isEqualTo(model.subId)
+                assertThat(subscriptionModel.cdmaLevel).isEqualTo(model.level)
+                assertThat(subscriptionModel.primaryLevel).isEqualTo(model.level)
+                assertThat(subscriptionModel.dataActivityDirection).isEqualTo(model.activity)
+                assertThat(subscriptionModel.carrierNetworkChangeActive)
+                    .isEqualTo(model.carrierNetworkChange)
+
+                // TODO(b/261029387) check these once we start handling them
+                assertThat(subscriptionModel.isEmergencyOnly).isFalse()
+                assertThat(subscriptionModel.isGsm).isFalse()
+                assertThat(subscriptionModel.dataConnectionState)
+                    .isEqualTo(DataConnectionState.Connected)
+            }
+            else -> {}
+        }
+    }
+}
+
+/** Convenience to create a valid fake network event with minimal params */
+fun validMobileEvent(
+    level: Int? = 1,
+    dataType: SignalIcon.MobileIconGroup? = THREE_G,
+    subId: Int? = 1,
+    carrierId: Int? = UNKNOWN_CARRIER_ID,
+    inflateStrength: Boolean? = false,
+    activity: Int? = null,
+    carrierNetworkChange: Boolean = false,
+): FakeNetworkEventModel =
+    FakeNetworkEventModel.Mobile(
+        level = level,
+        dataType = dataType,
+        subId = subId,
+        carrierId = carrierId,
+        inflateStrength = inflateStrength,
+        activity = activity,
+        carrierNetworkChange = carrierNetworkChange,
+    )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 5ce51bb..c8df5ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline.mobile.data.repository
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
 import android.os.UserHandle
 import android.provider.Settings
@@ -31,6 +31,7 @@
 import android.telephony.TelephonyManager.DATA_CONNECTING
 import android.telephony.TelephonyManager.DATA_DISCONNECTED
 import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DATA_UNKNOWN
 import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
@@ -39,6 +40,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
@@ -221,6 +223,21 @@
         }
 
     @Test
+    fun testFlowForSubId_dataConnectionState_unknown() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Unknown)
+
+            job.cancel()
+        }
+
+    @Test
     fun testFlowForSubId_dataActivity() =
         runBlocking(IMMEDIATE) {
             var latest: MobileSubscriptionModel? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
similarity index 99%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index a953a3d..359ea18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline.mobile.data.repository
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
 import android.content.Intent
 import android.net.ConnectivityManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index 5c16e129..3d9fd96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.lifecycle.InstantTaskExecutorRule
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
@@ -64,6 +65,7 @@
     private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock
     private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var tableLogBuffer: TableLogBuffer
     @Mock
     private lateinit var connectivityConstants: ConnectivityConstants
     @Mock
@@ -103,6 +105,7 @@
             connectivityConstants,
             context,
             logger,
+            tableLogBuffer,
             interactor,
             scope,
             statusBarPipelineFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 3001b81..12b93819 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -23,6 +23,7 @@
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
@@ -40,6 +41,7 @@
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
@@ -67,6 +69,7 @@
 
     @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var tableLogBuffer: TableLogBuffer
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
@@ -123,6 +126,7 @@
                     connectivityConstants,
                     context,
                     logger,
+                    tableLogBuffer,
                     interactor,
                     scope,
                     statusBarPipelineFlags,
@@ -137,15 +141,21 @@
             yield()
 
             // THEN we get the expected icon
-            assertThat(iconFlow.value?.res).isEqualTo(testCase.expected?.iconResource)
-            val expectedContentDescription =
-                if (testCase.expected == null) {
-                    null
-                } else {
-                    testCase.expected.contentDescription.invoke(context)
+            val actualIcon = iconFlow.value
+            when (testCase.expected) {
+                null -> {
+                    assertThat(actualIcon).isInstanceOf(WifiIcon.Hidden::class.java)
                 }
-            assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context))
-                .isEqualTo(expectedContentDescription)
+                else -> {
+                    assertThat(actualIcon).isInstanceOf(WifiIcon.Visible::class.java)
+                    val actualIconVisible = actualIcon as WifiIcon.Visible
+                    assertThat(actualIconVisible.icon.res).isEqualTo(testCase.expected.iconResource)
+                    val expectedContentDescription =
+                        testCase.expected.contentDescription.invoke(context)
+                    assertThat(actualIconVisible.contentDescription.loadContentDescription(context))
+                        .isEqualTo(expectedContentDescription)
+                }
+            }
 
             job.cancel()
         }
@@ -174,7 +184,7 @@
         val isDefault: Boolean = false,
         val network: WifiNetworkModel,
 
-        /** The expected output. Null if we expect the output to be null. */
+        /** The expected output. Null if we expect the output to be hidden. */
         val expected: Expected?
     ) {
         override fun toString(): String {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 6a6b2a8..7502020 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
@@ -34,6 +34,7 @@
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -59,6 +60,7 @@
 
     @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var tableLogBuffer: TableLogBuffer
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
@@ -103,21 +105,21 @@
 
     @Test
     fun wifiIcon_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
-        var latestHome: Icon? = null
+        var latestHome: WifiIcon? = null
         val jobHome = underTest
             .home
             .wifiIcon
             .onEach { latestHome = it }
             .launchIn(this)
 
-        var latestKeyguard: Icon? = null
+        var latestKeyguard: WifiIcon? = null
         val jobKeyguard = underTest
             .keyguard
             .wifiIcon
             .onEach { latestKeyguard = it }
             .launchIn(this)
 
-        var latestQs: Icon? = null
+        var latestQs: WifiIcon? = null
         val jobQs = underTest
             .qs
             .wifiIcon
@@ -133,7 +135,7 @@
         )
         yield()
 
-        assertThat(latestHome).isInstanceOf(Icon.Resource::class.java)
+        assertThat(latestHome).isInstanceOf(WifiIcon.Visible::class.java)
         assertThat(latestHome).isEqualTo(latestKeyguard)
         assertThat(latestKeyguard).isEqualTo(latestQs)
 
@@ -541,6 +543,7 @@
             connectivityConstants,
             context,
             logger,
+            tableLogBuffer,
             interactor,
             scope,
             statusBarPipelineFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt
new file mode 100644
index 0000000..01dd60a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/TestableAlertDialogTest.kt
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.util
+
+import android.content.DialogInterface
+import android.content.DialogInterface.BUTTON_NEGATIVE
+import android.content.DialogInterface.BUTTON_NEUTRAL
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class TestableAlertDialogTest : SysuiTestCase() {
+
+    @Test
+    fun dialogNotShowingWhenCreated() {
+        val dialog = TestableAlertDialog(context)
+
+        assertThat(dialog.isShowing).isFalse()
+    }
+
+    @Test
+    fun dialogShownDoesntCrash() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.show()
+    }
+
+    @Test
+    fun dialogShowing() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.show()
+
+        assertThat(dialog.isShowing).isTrue()
+    }
+
+    @Test
+    fun showListenerCalled() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnShowListener = mock()
+        dialog.setOnShowListener(listener)
+
+        dialog.show()
+
+        verify(listener).onShow(dialog)
+    }
+
+    @Test
+    fun showListenerRemoved() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnShowListener = mock()
+        dialog.setOnShowListener(listener)
+        dialog.setOnShowListener(null)
+
+        dialog.show()
+
+        verify(listener, never()).onShow(any())
+    }
+
+    @Test
+    fun dialogHiddenNotShowing() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.show()
+        dialog.hide()
+
+        assertThat(dialog.isShowing).isFalse()
+    }
+
+    @Test
+    fun dialogDismissNotShowing() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.show()
+        dialog.dismiss()
+
+        assertThat(dialog.isShowing).isFalse()
+    }
+
+    @Test
+    fun dismissListenerCalled_ifShowing() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnDismissListener = mock()
+        dialog.setOnDismissListener(listener)
+
+        dialog.show()
+        dialog.dismiss()
+
+        verify(listener).onDismiss(dialog)
+    }
+
+    @Test
+    fun dismissListenerNotCalled_ifNotShowing() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnDismissListener = mock()
+        dialog.setOnDismissListener(listener)
+
+        dialog.dismiss()
+
+        verify(listener, never()).onDismiss(any())
+    }
+
+    @Test
+    fun dismissListenerRemoved() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnDismissListener = mock()
+        dialog.setOnDismissListener(listener)
+        dialog.setOnDismissListener(null)
+
+        dialog.show()
+        dialog.dismiss()
+
+        verify(listener, never()).onDismiss(any())
+    }
+
+    @Test
+    fun cancelListenerCalled_showing() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnCancelListener = mock()
+        dialog.setOnCancelListener(listener)
+
+        dialog.show()
+        dialog.cancel()
+
+        verify(listener).onCancel(dialog)
+    }
+
+    @Test
+    fun cancelListenerCalled_notShowing() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnCancelListener = mock()
+        dialog.setOnCancelListener(listener)
+
+        dialog.cancel()
+
+        verify(listener).onCancel(dialog)
+    }
+
+    @Test
+    fun dismissCalledOnCancel_showing() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnDismissListener = mock()
+        dialog.setOnDismissListener(listener)
+
+        dialog.show()
+        dialog.cancel()
+
+        verify(listener).onDismiss(dialog)
+    }
+
+    @Test
+    fun dialogCancelNotShowing() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.show()
+        dialog.cancel()
+
+        assertThat(dialog.isShowing).isFalse()
+    }
+
+    @Test
+    fun cancelListenerRemoved() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnCancelListener = mock()
+        dialog.setOnCancelListener(listener)
+        dialog.setOnCancelListener(null)
+
+        dialog.show()
+        dialog.cancel()
+
+        verify(listener, never()).onCancel(any())
+    }
+
+    @Test
+    fun positiveButtonClick() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnClickListener = mock()
+        dialog.setButton(BUTTON_POSITIVE, "", listener)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_POSITIVE)
+
+        verify(listener).onClick(dialog, BUTTON_POSITIVE)
+    }
+
+    @Test
+    fun positiveButtonListener_noCalledWhenClickOtherButtons() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnClickListener = mock()
+        dialog.setButton(BUTTON_POSITIVE, "", listener)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_NEUTRAL)
+        dialog.clickButton(BUTTON_NEGATIVE)
+
+        verify(listener, never()).onClick(any(), anyInt())
+    }
+
+    @Test
+    fun negativeButtonClick() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnClickListener = mock()
+        dialog.setButton(BUTTON_NEGATIVE, "", listener)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_NEGATIVE)
+
+        verify(listener).onClick(dialog, DialogInterface.BUTTON_NEGATIVE)
+    }
+
+    @Test
+    fun negativeButtonListener_noCalledWhenClickOtherButtons() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnClickListener = mock()
+        dialog.setButton(BUTTON_NEGATIVE, "", listener)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_NEUTRAL)
+        dialog.clickButton(BUTTON_POSITIVE)
+
+        verify(listener, never()).onClick(any(), anyInt())
+    }
+
+    @Test
+    fun neutralButtonClick() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnClickListener = mock()
+        dialog.setButton(BUTTON_NEUTRAL, "", listener)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_NEUTRAL)
+
+        verify(listener).onClick(dialog, BUTTON_NEUTRAL)
+    }
+
+    @Test
+    fun neutralButtonListener_noCalledWhenClickOtherButtons() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnClickListener = mock()
+        dialog.setButton(BUTTON_NEUTRAL, "", listener)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_POSITIVE)
+        dialog.clickButton(BUTTON_NEGATIVE)
+
+        verify(listener, never()).onClick(any(), anyInt())
+    }
+
+    @Test
+    fun sameClickListenerCalledCorrectly() {
+        val dialog = TestableAlertDialog(context)
+        val listener: DialogInterface.OnClickListener = mock()
+        dialog.setButton(BUTTON_POSITIVE, "", listener)
+        dialog.setButton(BUTTON_NEUTRAL, "", listener)
+        dialog.setButton(BUTTON_NEGATIVE, "", listener)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_POSITIVE)
+        dialog.clickButton(BUTTON_NEGATIVE)
+        dialog.clickButton(BUTTON_NEUTRAL)
+
+        val inOrder = inOrder(listener)
+        inOrder.verify(listener).onClick(dialog, BUTTON_POSITIVE)
+        inOrder.verify(listener).onClick(dialog, BUTTON_NEGATIVE)
+        inOrder.verify(listener).onClick(dialog, BUTTON_NEUTRAL)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun clickBadButton() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.clickButton(10000)
+    }
+
+    @Test
+    fun clickButtonDismisses_positive() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_POSITIVE)
+
+        assertThat(dialog.isShowing).isFalse()
+    }
+
+    @Test
+    fun clickButtonDismisses_negative() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_NEGATIVE)
+
+        assertThat(dialog.isShowing).isFalse()
+    }
+
+    @Test
+    fun clickButtonDismisses_neutral() {
+        val dialog = TestableAlertDialog(context)
+
+        dialog.show()
+        dialog.clickButton(BUTTON_NEUTRAL)
+
+        assertThat(dialog.isShowing).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
new file mode 100644
index 0000000..3767fbe
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import android.os.Debug;
+import android.util.Log;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Convenience class for grabbing a heap dump after a test class is run.
+ *
+ * To use:
+ * - locally edit your test class to inherit from MemoryTrackingTestCase instead of SysuiTestCase
+ * - Watch the logcat with tag MEMORY to see the path to the .ahprof file
+ * - adb pull /path/to/something.ahprof
+ * - Download ahat from https://sites.google.com/corp/google.com/ahat/home
+ * - java -jar ~/Downloads/ahat-1.7.2.jar something.hprof
+ * - Watch output for next steps
+ * - Profit and fix leaks!
+ */
+public class MemoryTrackingTestCase extends SysuiTestCase {
+    private static File sFilesDir = null;
+    private static String sLatestTestClassName = null;
+
+    @Before public void grabFilesDir() {
+        if (sFilesDir == null) {
+            sFilesDir = mContext.getFilesDir();
+        }
+        sLatestTestClassName = getClass().getName();
+    }
+
+    @AfterClass
+    public static void dumpHeap() throws IOException {
+        if (sFilesDir == null) {
+            Log.e("MEMORY", "Somehow no test cases??");
+            return;
+        }
+        mockitoTearDown();
+        Log.w("MEMORY", "about to dump heap");
+        File path = new File(sFilesDir, sLatestTestClassName + ".ahprof");
+        Debug.dumpHprofData(path.getPath());
+        Log.w("MEMORY", "did it!  Location: " + path);
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
index 21e16a1..8a10bf06 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
@@ -81,11 +81,6 @@
         properties.get(namespace).put(name, value);
     }
 
-    @Override
-    public void enforceReadPermission(String namespace) {
-        // no-op
-    }
-
     private Properties propsForNamespaceAndName(String namespace, String name) {
         if (mProperties.containsKey(namespace) && mProperties.get(namespace).containsKey(name)) {
             return new Properties.Builder(namespace)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt
new file mode 100644
index 0000000..4d79554
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/TestableAlertDialog.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.util
+
+import android.app.AlertDialog
+import android.content.Context
+import android.content.DialogInterface
+import java.lang.IllegalArgumentException
+
+/**
+ * [AlertDialog] that is easier to test. Due to [AlertDialog] being a class and not an interface,
+ * there are some things that cannot be avoided, like the creation of a [Handler] on the main thread
+ * (and therefore needing a prepared [Looper] in the test).
+ *
+ * It bypasses calls to show, clicks on buttons, cancel and dismiss so it all can happen bounded in
+ * the test. It tries to be as close in behavior as a real [AlertDialog].
+ *
+ * It will only call [onCreate] as part of its lifecycle, but not any of the other lifecycle methods
+ * in [Dialog].
+ *
+ * In order to test clicking on buttons, use [clickButton] instead of calling [View.callOnClick] on
+ * the view returned by [getButton] to bypass the internal [Handler].
+ */
+class TestableAlertDialog(context: Context) : AlertDialog(context) {
+
+    private var _onDismissListener: DialogInterface.OnDismissListener? = null
+    private var _onCancelListener: DialogInterface.OnCancelListener? = null
+    private var _positiveButtonClickListener: DialogInterface.OnClickListener? = null
+    private var _negativeButtonClickListener: DialogInterface.OnClickListener? = null
+    private var _neutralButtonClickListener: DialogInterface.OnClickListener? = null
+    private var _onShowListener: DialogInterface.OnShowListener? = null
+    private var _dismissOverride: Runnable? = null
+
+    private var showing = false
+    private var visible = false
+    private var created = false
+
+    override fun show() {
+        if (!created) {
+            created = true
+            onCreate(null)
+        }
+        if (isShowing) return
+        showing = true
+        visible = true
+        _onShowListener?.onShow(this)
+    }
+
+    override fun hide() {
+        visible = false
+    }
+
+    override fun isShowing(): Boolean {
+        return visible && showing
+    }
+
+    override fun dismiss() {
+        if (!showing) {
+            return
+        }
+        if (_dismissOverride != null) {
+            _dismissOverride?.run()
+            return
+        }
+        _onDismissListener?.onDismiss(this)
+        showing = false
+    }
+
+    override fun cancel() {
+        _onCancelListener?.onCancel(this)
+        dismiss()
+    }
+
+    override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) {
+        _onDismissListener = listener
+    }
+
+    override fun setOnCancelListener(listener: DialogInterface.OnCancelListener?) {
+        _onCancelListener = listener
+    }
+
+    override fun setOnShowListener(listener: DialogInterface.OnShowListener?) {
+        _onShowListener = listener
+    }
+
+    override fun takeCancelAndDismissListeners(
+        msg: String?,
+        cancel: DialogInterface.OnCancelListener?,
+        dismiss: DialogInterface.OnDismissListener?
+    ): Boolean {
+        _onCancelListener = cancel
+        _onDismissListener = dismiss
+        return true
+    }
+
+    override fun setButton(
+        whichButton: Int,
+        text: CharSequence?,
+        listener: DialogInterface.OnClickListener?
+    ) {
+        super.setButton(whichButton, text, listener)
+        when (whichButton) {
+            DialogInterface.BUTTON_POSITIVE -> _positiveButtonClickListener = listener
+            DialogInterface.BUTTON_NEGATIVE -> _negativeButtonClickListener = listener
+            DialogInterface.BUTTON_NEUTRAL -> _neutralButtonClickListener = listener
+            else -> Unit
+        }
+    }
+
+    /**
+     * Click one of the buttons in the [AlertDialog] and call the corresponding listener.
+     *
+     * Button ids are from [DialogInterface].
+     */
+    fun clickButton(whichButton: Int) {
+        val listener =
+            when (whichButton) {
+                DialogInterface.BUTTON_POSITIVE -> _positiveButtonClickListener
+                DialogInterface.BUTTON_NEGATIVE -> _negativeButtonClickListener
+                DialogInterface.BUTTON_NEUTRAL -> _neutralButtonClickListener
+                else -> throw IllegalArgumentException("Wrong button $whichButton")
+            }
+        listener?.onClick(this, whichButton)
+        dismiss()
+    }
+}
diff --git a/proto/src/OWNERS b/proto/src/OWNERS
index abd08de..ccff624 100644
--- a/proto/src/OWNERS
+++ b/proto/src/OWNERS
@@ -2,3 +2,4 @@
 per-file wifi.proto = file:/wifi/OWNERS
 per-file camera.proto = file:/services/core/java/com/android/server/camera/OWNERS
 per-file system_messages.proto = file:/core/res/OWNERS
+per-file altitude.proto = file:/location/OWNERS
diff --git a/proto/src/altitude.proto b/proto/src/altitude.proto
new file mode 100644
index 0000000..1010f67
--- /dev/null
+++ b/proto/src/altitude.proto
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package com.android.internal.location.altitude;
+
+option java_package = "com.android.internal.location.altitude";
+option java_multiple_files = true;
+
+// Defines parameters for a spherically projected geoid map and corresponding
+// tile management.
+message MapParamsProto {
+  // Defines the resolution of the map in terms of an S2 level.
+  optional int32 map_s2_level = 1;
+  // Defines the resolution of the tiles in cache in terms of an S2 level.
+  optional int32 cache_tile_s2_level = 2;
+  // Defines the resolution of the tiles on disk in terms of an S2 level.
+  optional int32 disk_tile_s2_level = 3;
+  // Defines the `a` coefficient in the expression `a * map_value + b` used to
+  // calculate a geoid height in meters.
+  optional double model_a_meters = 4;
+  // Defines the `b` coefficient in the expression `a * map_value + b` used to
+  // calculate a geoid height in meters.
+  optional double model_b_meters = 5;
+  // Defines the root mean square error in meters of the geoid height.
+  optional double model_rmse_meters = 6;
+}
+
+// A single tile associating values in the unit interval [0, 1] to map cells.
+message S2TileProto {
+  // The S2 token associated with the common parent of all map cells in this
+  // tile.
+  optional string tile_key = 1;
+
+  // Encoded data that merge into a value in the unit interval [0, 1] for each
+  // map cell in this tile.
+  optional bytes byte_buffer = 2;
+  optional bytes byte_jpeg = 3;
+  optional bytes byte_png = 4;
+}
diff --git a/services/api/current.txt b/services/api/current.txt
index 834ed2f..18b1df3 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -59,6 +59,7 @@
     method public void close();
     method public void forAllPackageStates(@NonNull java.util.function.Consumer<com.android.server.pm.pkg.PackageState>);
     method @Nullable public com.android.server.pm.pkg.PackageState getPackageState(@NonNull String);
+    method @NonNull public java.util.Map<java.lang.String,com.android.server.pm.pkg.PackageState> getPackageStates();
   }
 
   public static interface PackageManagerLocal.UnfilteredSnapshot extends java.lang.AutoCloseable {
@@ -98,6 +99,7 @@
   public interface PackageState {
     method @Nullable public com.android.server.pm.pkg.AndroidPackage getAndroidPackage();
     method public int getAppId();
+    method public int getHiddenApiEnforcementPolicy();
     method @NonNull public String getPackageName();
     method @Nullable public String getPrimaryCpuAbi();
     method @Nullable public String getSeInfo();
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index f1ba5ff..5e68d52 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -16,8 +16,7 @@
 
 package com.android.server.companion.virtual;
 
-import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
-import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
+import static android.companion.virtual.VirtualDeviceParams.RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS;
 import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
@@ -26,10 +25,10 @@
 import android.annotation.Nullable;
 import android.app.WindowConfiguration;
 import android.app.compat.CompatChanges;
-import android.companion.AssociationRequest;
 import android.companion.virtual.VirtualDeviceManager.ActivityListener;
 import android.companion.virtual.VirtualDeviceParams;
 import android.companion.virtual.VirtualDeviceParams.ActivityPolicy;
+import android.companion.virtual.VirtualDeviceParams.RecentsPolicy;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.ComponentName;
@@ -127,10 +126,10 @@
     @GuardedBy("mGenericWindowPolicyControllerLock")
     private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListeners =
             new ArraySet<>();
-    @Nullable
-    private final @AssociationRequest.DeviceProfile String mDeviceProfile;
     @Nullable private final SecureWindowCallback mSecureWindowCallback;
     @Nullable private final List<String> mDisplayCategories;
+    @RecentsPolicy
+    private final int mDefaultRecentsPolicy;
 
     /**
      * Creates a window policy controller that is generic to the different use cases of virtual
@@ -156,7 +155,7 @@
      *   launching.
      * @param secureWindowCallback Callback that is called when a secure window shows on the
      *   virtual display.
-     * @param deviceProfile The {@link AssociationRequest.DeviceProfile} of this virtual device.
+     * @param defaultRecentsPolicy a policy to indicate how to handle activities in recents.
      */
     public GenericWindowPolicyController(int windowFlags, int systemWindowFlags,
             @NonNull ArraySet<UserHandle> allowedUsers,
@@ -169,8 +168,8 @@
             @NonNull PipBlockedCallback pipBlockedCallback,
             @NonNull ActivityBlockedCallback activityBlockedCallback,
             @NonNull SecureWindowCallback secureWindowCallback,
-            @AssociationRequest.DeviceProfile String deviceProfile,
-            @NonNull List<String> displayCategories) {
+            @NonNull List<String> displayCategories,
+            @RecentsPolicy int defaultRecentsPolicy) {
         super();
         mAllowedUsers = allowedUsers;
         mAllowedCrossTaskNavigations = new ArraySet<>(allowedCrossTaskNavigations);
@@ -181,10 +180,10 @@
         mActivityBlockedCallback = activityBlockedCallback;
         setInterestedWindowFlags(windowFlags, systemWindowFlags);
         mActivityListener = activityListener;
-        mDeviceProfile = deviceProfile;
         mPipBlockedCallback = pipBlockedCallback;
         mSecureWindowCallback = secureWindowCallback;
         mDisplayCategories = displayCategories;
+        mDefaultRecentsPolicy = defaultRecentsPolicy;
     }
 
     /**
@@ -318,18 +317,8 @@
     }
 
     @Override
-    public boolean canShowTasksInRecents() {
-        if (mDeviceProfile == null) {
-            return true;
-        }
-        // TODO(b/234075973) : Remove this once proper API is ready.
-        switch (mDeviceProfile) {
-            case DEVICE_PROFILE_AUTOMOTIVE_PROJECTION:
-                return false;
-            case DEVICE_PROFILE_APP_STREAMING:
-            default:
-                return true;
-        }
+    public boolean canShowTasksInHostDeviceRecents() {
+        return (mDefaultRecentsPolicy & RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS) != 0;
     }
 
     @Override
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 02053cc..0cea3d0 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -19,7 +19,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.StringDef;
-import android.graphics.Point;
 import android.graphics.PointF;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.InputDeviceIdentifier;
@@ -79,9 +78,8 @@
     final Object mLock;
 
     /* Token -> file descriptor associations. */
-    @VisibleForTesting
     @GuardedBy("mLock")
-    final Map<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors = new ArrayMap<>();
+    private final Map<IBinder, InputDeviceDescriptor> mInputDeviceDescriptors = new ArrayMap<>();
 
     private final Handler mHandler;
     private final NativeWrapper mNativeWrapper;
@@ -178,13 +176,14 @@
             int productId,
             @NonNull IBinder deviceToken,
             int displayId,
-            @NonNull Point screenSize) {
+            int height,
+            int width) {
         final String phys = createPhys(PHYS_TYPE_TOUCHSCREEN);
         try {
             createDeviceInternal(InputDeviceDescriptor.TYPE_TOUCHSCREEN, deviceName, vendorId,
                     productId, deviceToken, displayId, phys,
                     () -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
-                            phys, screenSize.y, screenSize.x));
+                            phys, height, width));
         } catch (DeviceCreationException e) {
             throw new RuntimeException(
                     "Failed to create virtual touchscreen device '" + deviceName + "'.", e);
@@ -414,17 +413,25 @@
     }
 
     @VisibleForTesting
-    void addDeviceForTesting(IBinder deviceToken, int fd, int type, int displayId,
-            String phys, int inputDeviceId) {
+    void addDeviceForTesting(IBinder deviceToken, int fd, int type, int displayId, String phys,
+            int inputDeviceId) {
         synchronized (mLock) {
-            mInputDeviceDescriptors.put(deviceToken,
-                    new InputDeviceDescriptor(fd, () -> {}, type, displayId, phys,
-                            inputDeviceId));
+            mInputDeviceDescriptors.put(deviceToken, new InputDeviceDescriptor(fd, () -> {
+            }, type, displayId, phys, inputDeviceId));
         }
     }
 
-    private static native int nativeOpenUinputDpad(String deviceName, int vendorId,
-            int productId, String phys);
+    @VisibleForTesting
+    Map<IBinder, InputDeviceDescriptor> getInputDeviceDescriptors() {
+        final Map<IBinder, InputDeviceDescriptor> inputDeviceDescriptors = new ArrayMap<>();
+        synchronized (mLock) {
+            inputDeviceDescriptors.putAll(mInputDeviceDescriptors);
+        }
+        return inputDeviceDescriptors;
+    }
+
+    private static native int nativeOpenUinputDpad(String deviceName, int vendorId, int productId,
+            String phys);
     private static native int nativeOpenUinputKeyboard(String deviceName, int vendorId,
             int productId, String phys);
     private static native int nativeOpenUinputMouse(String deviceName, int vendorId, int productId,
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 828f302..5819861 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -44,14 +44,17 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
-import android.graphics.Point;
 import android.graphics.PointF;
 import android.hardware.display.DisplayManager;
+import android.hardware.input.VirtualDpadConfig;
 import android.hardware.input.VirtualKeyEvent;
+import android.hardware.input.VirtualKeyboardConfig;
 import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
 import android.hardware.input.VirtualTouchEvent;
+import android.hardware.input.VirtualTouchscreenConfig;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.Looper;
@@ -397,19 +400,12 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @Override // Binder call
-    public void createVirtualDpad(
-            int displayId,
-            @NonNull String deviceName,
-            int vendorId,
-            int productId,
-            @NonNull IBinder deviceToken) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+    public void createVirtualDpad(VirtualDpadConfig config, @NonNull IBinder deviceToken) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
                 "Permission required to create a virtual dpad");
         synchronized (mVirtualDeviceLock) {
-            if (!mVirtualDisplayIds.contains(displayId)) {
+            if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
                         "Cannot create a virtual dpad for a display not associated with "
                                 + "this virtual device");
@@ -417,26 +413,19 @@
         }
         final long ident = Binder.clearCallingIdentity();
         try {
-            mInputController.createDpad(deviceName, vendorId, productId, deviceToken,
-                    displayId);
+            mInputController.createDpad(config.getInputDeviceName(), config.getVendorId(),
+                    config.getProductId(), deviceToken, config.getAssociatedDisplayId());
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @Override // Binder call
-    public void createVirtualKeyboard(
-            int displayId,
-            @NonNull String deviceName,
-            int vendorId,
-            int productId,
-            @NonNull IBinder deviceToken) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+    public void createVirtualKeyboard(VirtualKeyboardConfig config, @NonNull IBinder deviceToken) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
                 "Permission required to create a virtual keyboard");
         synchronized (mVirtualDeviceLock) {
-            if (!mVirtualDisplayIds.contains(displayId)) {
+            if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
                         "Cannot create a virtual keyboard for a display not associated with "
                                 + "this virtual device");
@@ -444,26 +433,19 @@
         }
         final long ident = Binder.clearCallingIdentity();
         try {
-            mInputController.createKeyboard(deviceName, vendorId, productId, deviceToken,
-                    displayId);
+            mInputController.createKeyboard(config.getInputDeviceName(), config.getVendorId(),
+                    config.getProductId(), deviceToken, config.getAssociatedDisplayId());
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @Override // Binder call
-    public void createVirtualMouse(
-            int displayId,
-            @NonNull String deviceName,
-            int vendorId,
-            int productId,
-            @NonNull IBinder deviceToken) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+    public void createVirtualMouse(VirtualMouseConfig config, @NonNull IBinder deviceToken) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
                 "Permission required to create a virtual mouse");
         synchronized (mVirtualDeviceLock) {
-            if (!mVirtualDisplayIds.contains(displayId)) {
+            if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
                         "Cannot create a virtual mouse for a display not associated with this "
                                 + "virtual device");
@@ -471,42 +453,38 @@
         }
         final long ident = Binder.clearCallingIdentity();
         try {
-            mInputController.createMouse(deviceName, vendorId, productId, deviceToken, displayId);
+            mInputController.createMouse(config.getInputDeviceName(), config.getVendorId(),
+                    config.getProductId(), deviceToken, config.getAssociatedDisplayId());
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @Override // Binder call
-    public void createVirtualTouchscreen(
-            int displayId,
-            @NonNull String deviceName,
-            int vendorId,
-            int productId,
-            @NonNull IBinder deviceToken,
-            @NonNull Point screenSize) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+    public void createVirtualTouchscreen(VirtualTouchscreenConfig config,
+            @NonNull IBinder deviceToken) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
                 "Permission required to create a virtual touchscreen");
         synchronized (mVirtualDeviceLock) {
-            if (!mVirtualDisplayIds.contains(displayId)) {
+            if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
                         "Cannot create a virtual touchscreen for a display not associated with "
                                 + "this virtual device");
             }
         }
-
-        if (screenSize.x <= 0 || screenSize.y <= 0) {
+        int screenHeightPixels = config.getHeightInPixels();
+        int screenWidthPixels = config.getWidthInPixels();
+        if (screenHeightPixels <= 0 || screenWidthPixels <= 0) {
             throw new IllegalArgumentException(
                     "Cannot create a virtual touchscreen, screen dimensions must be positive. Got: "
-                            + screenSize);
+                            + "(" + screenWidthPixels + ", " + screenHeightPixels + ")");
         }
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            mInputController.createTouchscreen(deviceName, vendorId, productId,
-                    deviceToken, displayId, screenSize);
+            mInputController.createTouchscreen(config.getInputDeviceName(), config.getVendorId(),
+                    config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
+                    screenHeightPixels, screenWidthPixels);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -701,8 +679,8 @@
                             this::onEnteringPipBlocked,
                             this::onActivityBlocked,
                             this::onSecureWindowShown,
-                            mAssociationInfo.getDeviceProfile(),
-                            displayCategories);
+                            displayCategories,
+                            mParams.getDefaultRecentsPolicy());
             gwpc.registerRunningAppsChangedListener(/* listener= */ this);
             return gwpc;
         }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index a61a61b..b6f0237 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -144,6 +144,7 @@
 
     static_libs: [
         "android.hardware.authsecret-V1.0-java",
+        "android.hardware.authsecret-V1-java",
         "android.hardware.boot-V1.0-java",
         "android.hardware.boot-V1.1-java",
         "android.hardware.boot-V1.2-java",
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bd90d85..32afcca 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1041,7 +1041,7 @@
             String callingFeatureId, IPhoneStateListener callback,
             int[] events, boolean notifyNow) {
         Set<Integer> eventList = Arrays.stream(events).boxed().collect(Collectors.toSet());
-        listen(renounceFineLocationAccess, renounceFineLocationAccess, callingPackage,
+        listen(renounceFineLocationAccess, renounceCoarseLocationAccess, callingPackage,
                 callingFeatureId, callback, eventList, notifyNow, subId);
     }
 
@@ -1606,7 +1606,7 @@
                             if (DBG) {
                                 log("notifyServiceStateForSubscriber: callback.onSSC r=" + r
                                         + " subId=" + subId + " phoneId=" + phoneId
-                                        + " state=" + state);
+                                        + " state=" + stateToSend);
                             }
                             r.callback.onServiceStateChanged(stateToSend);
                         } catch (RemoteException ex) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 2a44e30..0d672bd 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3016,7 +3016,6 @@
 
         final TimeoutRecord tr = TimeoutRecord.forShortFgsTimeout(reason);
 
-        // TODO(short-service): TODO Add SHORT_FGS_TIMEOUT to AnrLatencyTracker
         tr.mLatencyTracker.waitingOnAMSLockStarted();
         synchronized (mAm) {
             tr.mLatencyTracker.waitingOnAMSLockEnded();
@@ -3838,6 +3837,11 @@
                             throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
                                     + className + " is not an isolatedProcess");
                         }
+                        if (AppGlobals.getPackageManager().getPackageUid(callingPackage,
+                                0, userId) != callingUid) {
+                            throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
+                                    + "calling package not owned by calling UID ");
+                        }
                         // Run the service under the calling package's application.
                         ApplicationInfo aInfo = AppGlobals.getPackageManager().getApplicationInfo(
                                 callingPackage, ActivityManagerService.STOCK_PM_FLAGS, userId);
@@ -5421,6 +5425,13 @@
                 // This is a call from a service start...  take care of
                 // book-keeping.
                 r.callStart = true;
+
+                // Set the result to startCommandResult.
+                // START_TASK_REMOVED_COMPLETE is _not_ a result from onStartCommand(), so
+                // let's ignore.
+                if (res != Service.START_TASK_REMOVED_COMPLETE) {
+                    r.startCommandResult = res;
+                }
                 switch (res) {
                     case Service.START_STICKY_COMPATIBILITY:
                     case Service.START_STICKY: {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 7566bab..efe14f4 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18324,7 +18324,7 @@
 
     public void waitForBroadcastBarrier(@Nullable PrintWriter pw) {
         enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()");
-        BroadcastLoopers.waitForIdle(pw);
+        BroadcastLoopers.waitForBarrier(pw);
         for (BroadcastQueue queue : mBroadcastQueues) {
             queue.waitForBarrier(pw);
         }
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 1eebd01..f5d1c10 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -153,12 +153,27 @@
             "bcast_extra_running_urgent_process_queues";
     private static final int DEFAULT_EXTRA_RUNNING_URGENT_PROCESS_QUEUES = 1;
 
+    /**
+     * For {@link BroadcastQueueModernImpl}: Maximum number of consecutive urgent
+     * broadcast dispatches allowed before letting broadcasts in lower priority queue
+     * to be scheduled in order to avoid starvation.
+     */
     public int MAX_CONSECUTIVE_URGENT_DISPATCHES = DEFAULT_MAX_CONSECUTIVE_URGENT_DISPATCHES;
     private static final String KEY_MAX_CONSECUTIVE_URGENT_DISPATCHES =
             "bcast_max_consecutive_urgent_dispatches";
     private static final int DEFAULT_MAX_CONSECUTIVE_URGENT_DISPATCHES = 3;
 
     /**
+     * For {@link BroadcastQueueModernImpl}: Maximum number of consecutive normal
+     * broadcast dispatches allowed before letting broadcasts in lower priority queue
+     * to be scheduled in order to avoid starvation.
+     */
+    public int MAX_CONSECUTIVE_NORMAL_DISPATCHES = DEFAULT_MAX_CONSECUTIVE_NORMAL_DISPATCHES;
+    private static final String KEY_MAX_CONSECUTIVE_NORMAL_DISPATCHES =
+            "bcast_max_consecutive_normal_dispatches";
+    private static final int DEFAULT_MAX_CONSECUTIVE_NORMAL_DISPATCHES = 10;
+
+    /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of active broadcasts
      * to dispatch to a "running" process queue before we retire them back to
      * being "runnable" to give other processes a chance to run.
@@ -341,6 +356,9 @@
             MAX_CONSECUTIVE_URGENT_DISPATCHES = getDeviceConfigInt(
                     KEY_MAX_CONSECUTIVE_URGENT_DISPATCHES,
                     DEFAULT_MAX_CONSECUTIVE_URGENT_DISPATCHES);
+            MAX_CONSECUTIVE_NORMAL_DISPATCHES = getDeviceConfigInt(
+                    KEY_MAX_CONSECUTIVE_NORMAL_DISPATCHES,
+                    DEFAULT_MAX_CONSECUTIVE_NORMAL_DISPATCHES);
             MAX_RUNNING_ACTIVE_BROADCASTS = getDeviceConfigInt(KEY_MAX_RUNNING_ACTIVE_BROADCASTS,
                     DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS);
             MAX_PENDING_BROADCASTS = getDeviceConfigInt(KEY_MAX_PENDING_BROADCASTS,
@@ -396,6 +414,10 @@
                     TimeUtils.formatDuration(DELAY_URGENT_MILLIS)).println();
             pw.print(KEY_MAX_HISTORY_COMPLETE_SIZE, MAX_HISTORY_COMPLETE_SIZE).println();
             pw.print(KEY_MAX_HISTORY_SUMMARY_SIZE, MAX_HISTORY_SUMMARY_SIZE).println();
+            pw.print(KEY_MAX_CONSECUTIVE_URGENT_DISPATCHES,
+                    MAX_CONSECUTIVE_URGENT_DISPATCHES).println();
+            pw.print(KEY_MAX_CONSECUTIVE_NORMAL_DISPATCHES,
+                    MAX_CONSECUTIVE_NORMAL_DISPATCHES).println();
             pw.decreaseIndent();
             pw.println();
         }
diff --git a/services/core/java/com/android/server/am/BroadcastLoopers.java b/services/core/java/com/android/server/am/BroadcastLoopers.java
index b828720..a5535cb 100644
--- a/services/core/java/com/android/server/am/BroadcastLoopers.java
+++ b/services/core/java/com/android/server/am/BroadcastLoopers.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.MessageQueue;
@@ -30,6 +31,7 @@
 import java.io.PrintWriter;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
+import java.util.function.BiConsumer;
 
 /**
  * Collection of {@link Looper} that are known to be used for broadcast dispatch
@@ -73,19 +75,44 @@
      * still in the future are ignored for the purposes of the idle test.
      */
     public static void waitForIdle(@Nullable PrintWriter pw) {
+        waitForCondition(pw, (looper, latch) -> {
+            final MessageQueue queue = looper.getQueue();
+            queue.addIdleHandler(() -> {
+                latch.countDown();
+                return false;
+            });
+        });
+    }
+
+    /**
+     * Wait for all registered {@link Looper} instances to handle currently waiting messages.
+     * Note that {@link Message#when} still in the future are ignored for the purposes
+     * of the idle test.
+     */
+    public static void waitForBarrier(@Nullable PrintWriter pw) {
+        waitForCondition(pw, (looper, latch) -> {
+            (new Handler(looper)).post(() -> {
+                latch.countDown();
+            });
+        });
+    }
+
+    /**
+     * Wait for all registered {@link Looper} instances to meet a certain condition.
+     */
+    private static void waitForCondition(@Nullable PrintWriter pw,
+            @NonNull BiConsumer<Looper, CountDownLatch> condition) {
         final CountDownLatch latch;
         synchronized (sLoopers) {
             final int N = sLoopers.size();
             latch = new CountDownLatch(N);
             for (int i = 0; i < N; i++) {
-                final MessageQueue queue = sLoopers.valueAt(i).getQueue();
+                final Looper looper = sLoopers.valueAt(i);
+                final MessageQueue queue = looper.getQueue();
                 if (queue.isIdle()) {
                     latch.countDown();
                 } else {
-                    queue.addIdleHandler(() -> {
-                        latch.countDown();
-                        return false;
-                    });
+                    condition.accept(looper, latch);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 66d7fc9..672392d 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -153,6 +153,12 @@
     private int mActiveCountConsecutiveUrgent;
 
     /**
+     * Number of consecutive normal broadcasts that have been dispatched
+     * since the last offload dispatch.
+     */
+    private int mActiveCountConsecutiveNormal;
+
+    /**
      * Count of pending broadcasts of these various flavors.
      */
     private int mCountForeground;
@@ -551,48 +557,63 @@
      * Will thrown an exception if there are no pending broadcasts; relies on
      * {@link #isEmpty()} being false.
      */
-    SomeArgs removeNextBroadcast() {
+    private @Nullable SomeArgs removeNextBroadcast() {
         final ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
         if (queue == mPendingUrgent) {
             mActiveCountConsecutiveUrgent++;
-        } else {
+        } else if (queue == mPending) {
             mActiveCountConsecutiveUrgent = 0;
+            mActiveCountConsecutiveNormal++;
+        } else if (queue == mPendingOffload) {
+            mActiveCountConsecutiveUrgent = 0;
+            mActiveCountConsecutiveNormal = 0;
         }
-        return queue.removeFirst();
+        return !isQueueEmpty(queue) ? queue.removeFirst() : null;
     }
 
     @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() {
-        ArrayDeque<SomeArgs> nextUrgent = mPendingUrgent.isEmpty() ? null : mPendingUrgent;
-        ArrayDeque<SomeArgs> nextNormal = null;
-        if (!mPending.isEmpty()) {
-            nextNormal = mPending;
-        } else if (!mPendingOffload.isEmpty()) {
-            nextNormal = mPendingOffload;
+        final ArrayDeque<SomeArgs> nextNormal = queueForNextBroadcast(
+                mPending, mPendingOffload,
+                mActiveCountConsecutiveNormal, constants.MAX_CONSECUTIVE_NORMAL_DISPATCHES);
+        final ArrayDeque<SomeArgs> nextBroadcastQueue = queueForNextBroadcast(
+                mPendingUrgent, nextNormal,
+                mActiveCountConsecutiveUrgent, constants.MAX_CONSECUTIVE_URGENT_DISPATCHES);
+        return nextBroadcastQueue;
+    }
+
+    private @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast(
+            @Nullable ArrayDeque<SomeArgs> highPriorityQueue,
+            @Nullable ArrayDeque<SomeArgs> lowPriorityQueue,
+            int consecutiveHighPriorityCount,
+            int maxHighPriorityDispatchLimit) {
+        // nothing high priority pending, no further decisionmaking
+        if (isQueueEmpty(highPriorityQueue)) {
+            return lowPriorityQueue;
         }
-        // nothing urgent pending, no further decisionmaking
-        if (nextUrgent == null) {
-            return nextNormal;
-        }
-        // nothing but urgent pending, also no further decisionmaking
-        if (nextNormal == null) {
-            return nextUrgent;
+        // nothing but high priority pending, also no further decisionmaking
+        if (isQueueEmpty(lowPriorityQueue)) {
+            return highPriorityQueue;
         }
 
-        // Starvation mitigation: although we prioritize urgent broadcasts by default,
-        // we allow non-urgent deliveries to make steady progress even if urgent
-        // broadcasts are arriving faster than they can be dispatched.
+        // Starvation mitigation: although we prioritize high priority queues by default,
+        // we allow low priority queues to make steady progress even if broadcasts in
+        // high priority queue are arriving faster than they can be dispatched.
         //
-        // We do not try to defer to the next non-urgent broadcast if that broadcast
+        // We do not try to defer to the next broadcast in low priority queues if that broadcast
         // is ordered and still blocked on delivery to other recipients.
-        final SomeArgs nextNormalArgs = nextNormal.peekFirst();
-        final BroadcastRecord rNormal = (BroadcastRecord) nextNormalArgs.arg1;
-        final int nextNormalIndex = nextNormalArgs.argi1;
-        final BroadcastRecord rUrgent = (BroadcastRecord) nextUrgent.peekFirst().arg1;
-        final boolean canTakeNormal =
-                mActiveCountConsecutiveUrgent >= constants.MAX_CONSECUTIVE_URGENT_DISPATCHES
-                        && rNormal.enqueueTime <= rUrgent.enqueueTime
-                        && !blockedOnOrderedDispatch(rNormal, nextNormalIndex);
-        return canTakeNormal ? nextNormal : nextUrgent;
+        final SomeArgs nextLPArgs = lowPriorityQueue.peekFirst();
+        final BroadcastRecord nextLPRecord = (BroadcastRecord) nextLPArgs.arg1;
+        final int nextLPRecordIndex = nextLPArgs.argi1;
+        final BroadcastRecord nextHPRecord = (BroadcastRecord) highPriorityQueue.peekFirst().arg1;
+        final boolean isLPQueueEligible =
+                consecutiveHighPriorityCount >= maxHighPriorityDispatchLimit
+                        && nextLPRecord.enqueueTime <= nextHPRecord.enqueueTime
+                        && !blockedOnOrderedDispatch(nextLPRecord, nextLPRecordIndex);
+        return isLPQueueEligible ? lowPriorityQueue : highPriorityQueue;
+    }
+
+    private static boolean isQueueEmpty(@Nullable ArrayDeque<SomeArgs> queue) {
+        return (queue == null || queue.isEmpty());
     }
 
     /**
@@ -600,13 +621,13 @@
      */
     @Nullable SomeArgs peekNextBroadcast() {
         ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
-        return (queue != null) ? queue.peekFirst() : null;
+        return !isQueueEmpty(queue) ? queue.peekFirst() : null;
     }
 
     @VisibleForTesting
     @Nullable BroadcastRecord peekNextBroadcastRecord() {
         ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
-        return (queue != null) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
+        return !isQueueEmpty(queue) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
     }
 
     /**
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 5d5dbbb..e9340e9 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -220,10 +220,19 @@
 
     private void enqueueFinishReceiver(@NonNull BroadcastProcessQueue queue,
             @DeliveryState int deliveryState, @NonNull String reason) {
+        enqueueFinishReceiver(queue, queue.getActive(), queue.getActiveIndex(),
+                deliveryState, reason);
+    }
+
+    private void enqueueFinishReceiver(@NonNull BroadcastProcessQueue queue,
+            @NonNull BroadcastRecord r, int index,
+            @DeliveryState int deliveryState, @NonNull String reason) {
         final SomeArgs args = SomeArgs.obtain();
         args.arg1 = queue;
         args.argi1 = deliveryState;
         args.arg2 = reason;
+        args.arg3 = r;
+        args.argi2 = index;
         mLocalHandler.sendMessage(Message.obtain(mLocalHandler, MSG_FINISH_RECEIVER, args));
     }
 
@@ -271,8 +280,10 @@
                     final BroadcastProcessQueue queue = (BroadcastProcessQueue) args.arg1;
                     final int deliveryState = args.argi1;
                     final String reason = (String) args.arg2;
+                    final BroadcastRecord r = (BroadcastRecord) args.arg3;
+                    final int index = args.argi2;
                     args.recycle();
-                    finishReceiverLocked(queue, deliveryState, reason);
+                    finishReceiverLocked(queue, deliveryState, reason, r, index);
                 }
                 return true;
             }
@@ -729,10 +740,8 @@
     private void scheduleReceiverWarmLocked(@NonNull BroadcastProcessQueue queue) {
         checkState(queue.isActive(), "isActive");
 
-        final ProcessRecord app = queue.app;
         final BroadcastRecord r = queue.getActive();
         final int index = queue.getActiveIndex();
-        final Object receiver = r.receivers.get(index);
 
         if (r.terminalCount == 0) {
             r.dispatchTime = SystemClock.uptimeMillis();
@@ -740,26 +749,40 @@
             r.dispatchClockTime = System.currentTimeMillis();
         }
 
-        // If someone already finished this broadcast, finish immediately
+        if (maybeSkipReceiver(queue, r, index)) {
+            return;
+        }
+        dispatchReceivers(queue, r, index);
+    }
+
+    /**
+     * Examine a receiver and possibly skip it.  The method returns true if the receiver is
+     * skipped (and therefore no more work is required).
+     */
+    private boolean maybeSkipReceiver(BroadcastProcessQueue queue, BroadcastRecord r, int index) {
         final int oldDeliveryState = getDeliveryState(r, index);
+        final ProcessRecord app = queue.app;
+        final Object receiver = r.receivers.get(index);
+
+        // If someone already finished this broadcast, finish immediately
         if (isDeliveryStateTerminal(oldDeliveryState)) {
             enqueueFinishReceiver(queue, oldDeliveryState, "already terminal state");
-            return;
+            return true;
         }
 
         // Consider additional cases where we'd want to finish immediately
         if (app.isInFullBackup()) {
             enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup");
-            return;
+            return true;
         }
         if (mSkipPolicy.shouldSkip(r, receiver)) {
             enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "mSkipPolicy");
-            return;
+            return true;
         }
         final Intent receiverIntent = r.getReceiverIntent(receiver);
         if (receiverIntent == null) {
             enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "getReceiverIntent");
-            return;
+            return true;
         }
 
         // Ignore registered receivers from a previous PID
@@ -767,12 +790,29 @@
                 && ((BroadcastFilter) receiver).receiverList.pid != app.getPid()) {
             enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED,
                     "BroadcastFilter for mismatched PID");
-            return;
+            return true;
         }
+        // The receiver was not handled in this method.
+        return false;
+    }
+
+    /**
+     * Return true if this receiver should be assumed to have been delivered.
+     */
+    private boolean isAssumedDelivered(BroadcastRecord r, int index) {
+        return (r.receivers.get(index) instanceof BroadcastFilter) && !r.ordered;
+    }
+
+    /**
+     * A receiver is about to be dispatched.  Start ANR timers, if necessary.
+     */
+    private void dispatchReceivers(BroadcastProcessQueue queue, BroadcastRecord r, int index) {
+        final ProcessRecord app = queue.app;
+        final Object receiver = r.receivers.get(index);
 
         // Skip ANR tracking early during boot, when requested, or when we
         // immediately assume delivery success
-        final boolean assumeDelivered = (receiver instanceof BroadcastFilter) && !r.ordered;
+        final boolean assumeDelivered = isAssumedDelivered(r, index);
         if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) {
             queue.lastCpuDelayTime = queue.app.getCpuDelayTime();
 
@@ -805,6 +845,7 @@
         setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED,
                 "scheduleReceiverWarmLocked");
 
+        final Intent receiverIntent = r.getReceiverIntent(receiver);
         final IApplicationThread thread = app.getOnewayThread();
         if (thread != null) {
             try {
@@ -920,6 +961,19 @@
         return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app");
     }
 
+    /**
+     * Return true if there are more broadcasts in the queue and the queue is runnable.
+     */
+    private boolean shouldContinueScheduling(@NonNull BroadcastProcessQueue queue) {
+        // If we've made reasonable progress, periodically retire ourselves to
+        // avoid starvation of other processes and stack overflow when a
+        // broadcast is immediately finished without waiting
+        final boolean shouldRetire =
+                (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
+
+        return queue.isRunnable() && queue.isProcessWarm() && !shouldRetire;
+    }
+
     private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue,
             @DeliveryState int deliveryState, @NonNull String reason) {
         if (!queue.isActive()) {
@@ -927,10 +981,21 @@
             return false;
         }
 
-        final int cookie = traceBegin("finishReceiver");
-        final ProcessRecord app = queue.app;
         final BroadcastRecord r = queue.getActive();
         final int index = queue.getActiveIndex();
+        return finishReceiverLocked(queue, deliveryState, reason, r, index);
+    }
+
+    private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue,
+            @DeliveryState int deliveryState, @NonNull String reason,
+            BroadcastRecord r, int index) {
+        if (!queue.isActive()) {
+            logw("Ignoring finish; no active broadcast for " + queue);
+            return false;
+        }
+
+        final int cookie = traceBegin("finishReceiver");
+        final ProcessRecord app = queue.app;
         final Object receiver = r.receivers.get(index);
 
         setDeliveryState(queue, app, r, index, receiver, deliveryState, reason);
@@ -945,18 +1010,11 @@
             mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_HARD, queue);
         }
 
-        // If we've made reasonable progress, periodically retire ourselves to
-        // avoid starvation of other processes and stack overflow when a
-        // broadcast is immediately finished without waiting
-        final boolean shouldRetire =
-                (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
-
-        final boolean res;
-        if (queue.isRunnable() && queue.isProcessWarm() && !shouldRetire) {
+        final boolean res = shouldContinueScheduling(queue);
+        if (res) {
             // We're on a roll; move onto the next broadcast for this process
             queue.makeActiveNextPending();
             scheduleReceiverWarmLocked(queue);
-            res = true;
         } else {
             // We've drained running broadcasts; maybe move back to runnable
             queue.makeActiveIdle();
@@ -970,7 +1028,6 @@
             // Tell other OS components that app is not actively running, giving
             // a chance to update OOM adjustment
             notifyStoppedRunning(queue);
-            res = false;
         }
         traceEnd(cookie);
         return res;
@@ -1442,10 +1499,10 @@
 
     private void notifyFinishBroadcast(@NonNull BroadcastRecord r) {
         mService.notifyBroadcastFinishedLocked(r);
-        mHistory.addBroadcastToHistoryLocked(r);
-
         r.finishTime = SystemClock.uptimeMillis();
         r.nextReceiver = r.receivers.size();
+        mHistory.addBroadcastToHistoryLocked(r);
+
         BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r);
 
         if (r.intent.getComponent() == null && r.intent.getPackage() == null
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 0d1a6d5..ef195aa 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -128,6 +128,7 @@
     boolean delayedStop;    // service has been stopped but is in a delayed start?
     boolean stopIfKilled;   // last onStart() said to stop if service killed?
     boolean callStart;      // last onStart() has asked to always be called on restart.
+    int startCommandResult; // last result from onStartCommand(), only for dumpsys.
     int executeNesting;     // number of outstanding operations keeping foreground.
     boolean executeFg;      // should we be executing in the foreground?
     long executingStart;    // start time of last execute request.
@@ -478,6 +479,7 @@
             proto.write(ServiceRecordProto.Start.DELAYED_STOP, delayedStop);
             proto.write(ServiceRecordProto.Start.STOP_IF_KILLED, stopIfKilled);
             proto.write(ServiceRecordProto.Start.LAST_START_ID, lastStartId);
+            proto.write(ServiceRecordProto.Start.START_COMMAND_RESULT, startCommandResult);
             proto.end(startToken);
         }
 
@@ -533,9 +535,22 @@
                 }
             }
         }
-        proto.end(token);
+        if (mShortFgsInfo != null && mShortFgsInfo.isCurrent()) {
+            final long shortFgsToken = proto.start(ServiceRecordProto.SHORT_FGS_INFO);
+            proto.write(ServiceRecordProto.ShortFgsInfo.START_TIME,
+                    mShortFgsInfo.getStartTime());
+            proto.write(ServiceRecordProto.ShortFgsInfo.START_ID,
+                    mShortFgsInfo.getStartId());
+            proto.write(ServiceRecordProto.ShortFgsInfo.TIMEOUT_TIME,
+                    mShortFgsInfo.getTimeoutTime());
+            proto.write(ServiceRecordProto.ShortFgsInfo.PROC_STATE_DEMOTE_TIME,
+                    mShortFgsInfo.getProcStateDemoteTime());
+            proto.write(ServiceRecordProto.ShortFgsInfo.ANR_TIME,
+                    mShortFgsInfo.getAnrTime());
+            proto.end(shortFgsToken);
+        }
 
-        // TODO(short-service) Add FGS info
+        proto.end(token);
     }
 
     void dump(PrintWriter pw, String prefix) {
@@ -632,6 +647,7 @@
                     pw.print(" stopIfKilled="); pw.print(stopIfKilled);
                     pw.print(" callStart="); pw.print(callStart);
                     pw.print(" lastStartId="); pw.println(lastStartId);
+                    pw.print(" startCommandResult="); pw.println(startCommandResult);
         }
         if (executeNesting != 0) {
             pw.print(prefix); pw.print("executeNesting="); pw.print(executeNesting);
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index 060e3ee..2a69363 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -18,6 +18,34 @@
       ]
     },
     {
+      "name": "CtsAppFgsTestCases",
+      "options": [
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
+      "name": "CtsShortFgsTestCases",
+      "options": [
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
       "name": "FrameworksServicesTests",
       "options": [
         {
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index a7e95fb..aefa2f5 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -949,9 +949,8 @@
     int stopUser(final int userId, final boolean force, boolean allowDelayedLocking,
             final IStopUserCallback stopUserCallback, KeyEvictedCallback keyEvictedCallback) {
         checkCallingPermission(INTERACT_ACROSS_USERS_FULL, "stopUser");
-        if (userId < 0 || userId == UserHandle.USER_SYSTEM) {
-            throw new IllegalArgumentException("Can't stop system user " + userId);
-        }
+        Preconditions.checkArgument(userId >= 0, "Invalid user id %d", userId);
+
         enforceShellRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES, userId);
         synchronized (mLock) {
             return stopUsersLU(userId, force, allowDelayedLocking, stopUserCallback,
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
index 8d1da71..587fb04 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
@@ -218,7 +218,7 @@
     }
 
     @Override
-    public boolean arePackageModesDefault(String packageMode, @UserIdInt int userId) {
+    public boolean arePackageModesDefault(@NonNull String packageMode, @UserIdInt int userId) {
         synchronized (mLock) {
             ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
             if (packageModes == null) {
@@ -490,15 +490,16 @@
     }
 
     @Override
-    public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) {
+    public SparseBooleanArray evalForegroundUidOps(int uid,
+            @Nullable SparseBooleanArray foregroundOps) {
         synchronized (mLock) {
             return evalForegroundOps(mUidModes.get(uid), foregroundOps);
         }
     }
 
     @Override
-    public SparseBooleanArray evalForegroundPackageOps(String packageName,
-            SparseBooleanArray foregroundOps, @UserIdInt int userId) {
+    public SparseBooleanArray evalForegroundPackageOps(@NonNull String packageName,
+            @Nullable SparseBooleanArray foregroundOps, @UserIdInt int userId) {
         synchronized (mLock) {
             ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
             return evalForegroundOps(packageModes == null ? null : packageModes.get(packageName),
@@ -537,8 +538,8 @@
     }
 
     @Override
-    public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage,
-            PrintWriter printWriter) {
+    public boolean dumpListeners(int dumpOp, int dumpUid, @Nullable String dumpPackage,
+            @NonNull PrintWriter printWriter) {
         boolean needSep = false;
         if (mOpModeWatchers.size() > 0) {
             boolean printedHeader = false;
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
index d8d0d48..ef3e368 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java
@@ -103,7 +103,7 @@
      * @param packageName package name.
      * @param userId user id associated with the package.
      */
-    boolean arePackageModesDefault(String packageName, @UserIdInt int userId);
+    boolean arePackageModesDefault(@NonNull String packageName, @UserIdInt int userId);
 
     /**
      * Stop tracking app-op modes for all uid and packages.
@@ -184,7 +184,7 @@
      * @param foregroundOps boolean array where app-ops that have MODE_FOREGROUND are marked true.
      * @return  foregroundOps.
      */
-    SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps);
+    SparseBooleanArray evalForegroundUidOps(int uid, @Nullable SparseBooleanArray foregroundOps);
 
     /**
      * Go over the list of app-ops for the package name and mark app-ops with MODE_FOREGROUND in
@@ -194,8 +194,8 @@
      * @param userId user id associated with the package.
      * @return foregroundOps.
      */
-    SparseBooleanArray evalForegroundPackageOps(String packageName,
-            SparseBooleanArray foregroundOps, @UserIdInt int userId);
+    SparseBooleanArray evalForegroundPackageOps(@NonNull String packageName,
+            @Nullable SparseBooleanArray foregroundOps, @UserIdInt int userId);
 
     /**
      * Dump op mode and package mode listeners and their details.
@@ -205,5 +205,6 @@
      * @param dumpPackage if not null and if dumpOp is -1, dumps watchers for the package name.
      * @param printWriter writer to dump to.
      */
-    boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, PrintWriter printWriter);
+    boolean dumpListeners(int dumpOp, int dumpUid, @Nullable String dumpPackage,
+            @NonNull PrintWriter printWriter);
 }
diff --git a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
index 5ebe811..1d1a9e7 100644
--- a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
+++ b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
@@ -22,7 +22,7 @@
  * Listener for mode changes, encapsulates methods that should be triggered in the event of a mode
  * change.
  */
-abstract class OnOpModeChangedListener {
+public abstract class OnOpModeChangedListener {
 
     // Constant meaning that any UID should be matched when dispatching callbacks
     private static final int UID_ANY = -2;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 8aa898e..aa8ee3d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -86,6 +86,7 @@
 import android.media.AudioFocusInfo;
 import android.media.AudioFocusRequest;
 import android.media.AudioFormat;
+import android.media.AudioHalVersionInfo;
 import android.media.AudioManager;
 import android.media.AudioManagerInternal;
 import android.media.AudioPlaybackConfiguration;
@@ -257,6 +258,9 @@
     /** Debug communication route */
     protected static final boolean DEBUG_COMM_RTE = false;
 
+    /** Debug log sound fx (touchsounds...) in dumpsys */
+    protected static final boolean DEBUG_LOG_SOUND_FX = false;
+
     /** How long to delay before persisting a change in volume/ringer mode. */
     private static final int PERSIST_DELAY = 500;
 
@@ -378,6 +382,7 @@
     private static final int MSG_ROTATION_UPDATE = 48;
     private static final int MSG_FOLD_UPDATE = 49;
     private static final int MSG_RESET_SPATIALIZER = 50;
+    private static final int MSG_NO_LOG_FOR_PLAYER_I = 51;
 
     // start of messages handled under wakelock
     //   these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
@@ -1016,7 +1021,7 @@
         PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
         mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent");
 
-        mSfxHelper = new SoundEffectsHelper(mContext);
+        mSfxHelper = new SoundEffectsHelper(mContext, playerBase -> ignorePlayerLogs(playerBase));
 
         final boolean headTrackingDefault = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_spatial_audio_head_tracking_enabled_default);
@@ -1500,6 +1505,18 @@
     }
 
     //-----------------------------------------------------------------
+    // Communicate to PlayackActivityMonitor whether to log or not
+    // the sound FX activity (useful for removing touch sounds in the activity logs)
+    void ignorePlayerLogs(@NonNull PlayerBase playerToIgnore) {
+        if (DEBUG_LOG_SOUND_FX) {
+            return;
+        }
+        sendMsg(mAudioHandler, MSG_NO_LOG_FOR_PLAYER_I, SENDMSG_REPLACE,
+                /*arg1, piid of the player*/ playerToIgnore.getPlayerIId(),
+                /*arg2 ignored*/ 0, /*obj ignored*/ null, /*delay*/ 0);
+    }
+
+    //-----------------------------------------------------------------
     // monitoring requests for volume range initialization
     @Override // AudioSystemAdapter.OnVolRangeInitRequestListener
     public void onVolumeRangeInitRequestFromNative() {
@@ -6622,9 +6639,13 @@
                     return AudioSystem.STREAM_RING;
                 } else if (wasStreamActiveRecently(
                         AudioSystem.STREAM_NOTIFICATION, sStreamOverrideDelayMs)) {
-                    if (DEBUG_VOL)
-                        Log.v(TAG, "getActiveStreamType: Forcing STREAM_NOTIFICATION stream active");
-                    return AudioSystem.STREAM_NOTIFICATION;
+                        if (DEBUG_VOL) {
+                            Log.v(
+                                    TAG,
+                                    "getActiveStreamType: Forcing STREAM_NOTIFICATION stream"
+                                            + " active");
+                        }
+                        return AudioSystem.STREAM_NOTIFICATION;
                 } else {
                     if (DEBUG_VOL) {
                         Log.v(TAG, "getActiveStreamType: Forcing DEFAULT_VOL_STREAM_NO_PLAYBACK("
@@ -8721,6 +8742,10 @@
                     // fold parameter format: "device_folded=x" where x is one of on, off
                     mAudioSystem.setParameters((String) msg.obj);
                     break;
+
+                case MSG_NO_LOG_FOR_PLAYER_I:
+                    mPlaybackMonitor.ignorePlayerIId(msg.arg1);
+                    break;
             }
         }
     }
@@ -10913,6 +10938,21 @@
     }
 
     /**
+     * Called by an AudioPolicyProxy when the client dies.
+     * Checks if an active playback for media use case is currently routed to one of the
+     * remote submix devices owned by this dynamic policy and broadcasts a becoming noisy
+     * intend in this case.
+     * @param addresses list of remote submix device addresses to check.
+     */
+    private void onPolicyClientDeath(List<String> addresses) {
+        for (String address : addresses) {
+            if (mPlaybackMonitor.hasActiveMediaPlaybackOnSubmixWithAddress(address)) {
+                mDeviceBroker.postBroadcastBecomingNoisy();
+                return;
+            }
+        }
+    }
+    /**
      * Apps with MODIFY_AUDIO_ROUTING can register any policy.
      * Apps with an audio capable MediaProjection are allowed to register a RENDER|LOOPBACK policy
      * as those policy do not modify the audio routing.
@@ -11284,15 +11324,16 @@
         return mMediaFocusControl.sendFocusLoss(focusLoser);
     }
 
-    private static final String[] HAL_VERSIONS =
-            new String[] {"7.1", "7.0", "6.0", "5.0", "4.0", "2.0"};
-
-    /** @see AudioManager#getHalVersion */
-    public @Nullable String getHalVersion() {
-        for (String version : HAL_VERSIONS) {
+    /**
+     * @see AudioManager#getHalVersion
+     */
+    public @Nullable AudioHalVersionInfo getHalVersion() {
+        for (AudioHalVersionInfo version : AudioHalVersionInfo.VERSIONS) {
             try {
+                // TODO: check AIDL service.
+                String versionStr = version.getMajorVersion() + "." + version.getMinorVersion();
                 HwBinder.getService(
-                        String.format("android.hardware.audio@%s::IDevicesFactory", version),
+                        String.format("android.hardware.audio@%s::IDevicesFactory", versionStr),
                         "default");
                 return version;
             } catch (NoSuchElementException e) {
@@ -11622,6 +11663,13 @@
         public void binderDied() {
             mDynPolicyLogger.enqueue((new EventLogger.StringEvent("AudioPolicy "
                     + mPolicyCallback.asBinder() + " died").printLog(TAG)));
+
+            List<String> addresses = new ArrayList<>();
+            for (AudioMix mix : mMixes) {
+                addresses.add(mix.getRegistration());
+            }
+            onPolicyClientDeath(addresses);
+
             release();
         }
 
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 0bc4b20..f35931ca 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -32,6 +32,7 @@
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
 import android.media.AudioPlaybackConfiguration.PlayerMuteEvent;
@@ -191,6 +192,18 @@
     }
 
     //=================================================================
+    // Player to ignore (only handling single player, designed for ignoring
+    // in the logs one specific player such as the touch sounds player)
+    @GuardedBy("mPlayerLock")
+    private ArrayList<Integer> mDoNotLogPiidList = new ArrayList<>();
+
+    /*package*/ void ignorePlayerIId(int doNotLogPiid) {
+        synchronized (mPlayerLock) {
+            mDoNotLogPiidList.add(doNotLogPiid);
+        }
+    }
+
+    //=================================================================
     // Track players and their states
     // methods playerAttributes, playerEvent, releasePlayer are all oneway calls
     //  into AudioService. They trigger synchronous dispatchPlaybackChange() which updates
@@ -314,14 +327,18 @@
             Log.v(TAG, TextUtils.formatSimple("playerEvent(piid=%d, event=%s, eventValue=%d)",
                     piid, AudioPlaybackConfiguration.playerStateToString(event), eventValue));
         }
-
-        final boolean change;
+        boolean change;
         synchronized(mPlayerLock) {
             final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
             if (apc == null) {
                 return;
             }
 
+            final boolean doNotLog = mDoNotLogPiidList.contains(piid);
+            if (doNotLog && event != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
+                // do not log nor dispatch events for "ignored" players other than the release
+                return;
+            }
             sEventLogger.enqueue(new PlayerEvent(piid, event, eventValue));
 
             if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_PORT_ID) {
@@ -338,7 +355,8 @@
                     }
                 }
             }
-            if (apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+            if (apc.getPlayerType() == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL
+                    && event != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
                 // FIXME SoundPool not ready for state reporting
                 return;
             }
@@ -350,9 +368,15 @@
                 Log.e(TAG, "Error handling event " + event);
                 change = false;
             }
-            if (change && event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
-                mDuckingManager.checkDuck(apc);
-                mFadingManager.checkFade(apc);
+            if (change) {
+                if (event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+                    mDuckingManager.checkDuck(apc);
+                    mFadingManager.checkFade(apc);
+                }
+                if (doNotLog) {
+                    // do not dispatch events for "ignored" players
+                    change = false;
+                }
             }
         }
         if (change) {
@@ -435,6 +459,10 @@
                 mEventHandler.sendMessage(
                         mEventHandler.obtainMessage(MSG_L_CLEAR_PORTS_FOR_PIID, piid, /*arg2=*/0));
 
+                if (change && mDoNotLogPiidList.contains(piid)) {
+                    // do not dispatch a change for a "do not log" player
+                    change = false;
+                }
             }
         }
         if (change) {
@@ -542,6 +570,26 @@
         return false;
     }
 
+    /**
+     * Return true if an active playback for media use case is currently routed to
+     * a remote submix device with the supplied address.
+     * @param address
+     */
+    public boolean hasActiveMediaPlaybackOnSubmixWithAddress(@NonNull String address) {
+        synchronized (mPlayerLock) {
+            for (AudioPlaybackConfiguration apc : mPlayers.values()) {
+                AudioDeviceInfo device = apc.getAudioDeviceInfo();
+                if (apc.getAudioAttributes().getUsage() == AudioAttributes.USAGE_MEDIA
+                        && apc.isActive() && device != null
+                        && device.getInternalType() == AudioSystem.DEVICE_OUT_REMOTE_SUBMIX
+                        && address.equals(device.getAddress())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     protected void dump(PrintWriter pw) {
         // players
         pw.println("\nPlaybackActivityMonitor dump time: "
@@ -560,6 +608,9 @@
             for (Integer piidInt : piidIntList) {
                 final AudioPlaybackConfiguration apc = mPlayers.get(piidInt);
                 if (apc != null) {
+                    if (mDoNotLogPiidList.contains(apc.getPlayerInterfaceId())) {
+                        pw.print("(not logged)");
+                    }
                     apc.dump(pw);
                 }
             }
diff --git a/services/core/java/com/android/server/audio/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java
index 79b54eb..8c4efba 100644
--- a/services/core/java/com/android/server/audio/SoundEffectsHelper.java
+++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java
@@ -25,6 +25,7 @@
 import android.media.MediaPlayer;
 import android.media.MediaPlayer.OnCompletionListener;
 import android.media.MediaPlayer.OnErrorListener;
+import android.media.PlayerBase;
 import android.media.SoundPool;
 import android.os.Environment;
 import android.os.Handler;
@@ -47,6 +48,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 
 /**
  * A helper class for managing sound effects loading / unloading
@@ -109,11 +111,14 @@
     private final int[] mEffects = new int[AudioManager.NUM_SOUND_EFFECTS]; // indexes in mResources
     private SoundPool mSoundPool;
     private SoundPoolLoader mSoundPoolLoader;
+    /** callback to provide handle to the player of the sound effects */
+    private final Consumer<PlayerBase> mPlayerAvailableCb;
 
-    SoundEffectsHelper(Context context) {
+    SoundEffectsHelper(Context context, Consumer<PlayerBase> playerAvailableCb) {
         mContext = context;
         mSfxAttenuationDb = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_soundEffectVolumeDb);
+        mPlayerAvailableCb = playerAvailableCb;
         startWorker();
     }
 
@@ -189,6 +194,7 @@
                         .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                         .build())
                 .build();
+        mPlayerAvailableCb.accept(mSoundPool);
         loadSoundAssets();
 
         mSoundPoolLoader = new SoundPoolLoader();
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index eb81e70..dcc98e1 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -27,6 +27,7 @@
 import android.accounts.AccountManagerInternal;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.app.AppGlobals;
@@ -65,6 +66,7 @@
 import android.content.pm.RegisteredServicesCacheListener;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.database.ContentObserver;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
@@ -88,6 +90,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.WorkSource;
+import android.provider.ContactsContract;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.text.format.TimeMigrationUtils;
@@ -99,6 +102,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
@@ -498,7 +502,7 @@
             }
             mJobScheduler = (JobScheduler) mContext.getSystemService(
                     Context.JOB_SCHEDULER_SERVICE);
-            mJobSchedulerInternal = LocalServices.getService(JobSchedulerInternal.class);
+            mJobSchedulerInternal = getJobSchedulerInternal();
             // Get all persisted syncs from JobScheduler
             List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs();
 
@@ -536,6 +540,11 @@
         }
     }
 
+    @VisibleForTesting
+    protected JobSchedulerInternal getJobSchedulerInternal() {
+        return LocalServices.getService(JobSchedulerInternal.class);
+    }
+
     /**
      * @return whether the device most likely has some periodic syncs.
      */
@@ -645,7 +654,7 @@
         mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
         mAccountManager = (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE);
-        mAccountManagerInternal = LocalServices.getService(AccountManagerInternal.class);
+        mAccountManagerInternal = getAccountManagerInternal();
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mAmi = LocalServices.getService(ActivityManagerInternal.class);
 
@@ -719,6 +728,11 @@
         mLogger.log("Sync manager initialized: " + Build.FINGERPRINT);
     }
 
+    @VisibleForTesting
+    protected AccountManagerInternal getAccountManagerInternal() {
+        return LocalServices.getService(AccountManagerInternal.class);
+    }
+
     public void onStartUser(int userId) {
         // Log on the handler to avoid slowing down device boot.
         mSyncHandler.post(() -> mLogger.log("onStartUser: user=", userId));
@@ -800,9 +814,44 @@
         return mSyncStorageEngine;
     }
 
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private boolean areContactWritesEnabledForUser(UserInfo userInfo) {
+        final UserManager um = UserManager.get(mContext);
+        try {
+            final UserProperties userProperties = um.getUserProperties(userInfo.getUserHandle());
+            return !userProperties.getUseParentsContacts();
+        } catch (IllegalArgumentException e) {
+            Log.w(TAG, "Trying to fetch user properties for non-existing/partial user "
+                    + userInfo.getUserHandle());
+            return false;
+        }
+    }
+
+    /**
+     * Check if account sync should be disabled for the given user and provider.
+     * @param userInfo
+     * @param providerName
+     * @return true if sync for the account corresponding to the given user and provider should be
+     * disabled, false otherwise. Also returns false if either of the inputs are null.
+     */
+    @VisibleForTesting
+    protected boolean shouldDisableSyncForUser(UserInfo userInfo, String providerName) {
+        if (userInfo == null || providerName == null) return false;
+        return providerName.equals(ContactsContract.AUTHORITY)
+                && !areContactWritesEnabledForUser(userInfo);
+    }
+
     private int getIsSyncable(Account account, int userId, String providerName) {
         int isSyncable = mSyncStorageEngine.getIsSyncable(account, userId, providerName);
-        UserInfo userInfo = UserManager.get(mContext).getUserInfo(userId);
+        final UserManager um = UserManager.get(mContext);
+        UserInfo userInfo = um.getUserInfo(userId);
+
+        // Check if the provider is allowed to sync data from linked accounts for the user
+        if (shouldDisableSyncForUser(userInfo, providerName)) {
+            Log.w(TAG, "Account sync is disabled for account: " + account
+                    + " userId: " + userId + " provider: " + providerName);
+            return AuthorityInfo.NOT_SYNCABLE;
+        }
 
         // If it's not a restricted user, return isSyncable.
         if (userInfo == null || !userInfo.isRestricted()) return isSyncable;
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index d499d01..197c64e 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -602,6 +602,14 @@
         mAmbientBrightnessThresholdsIdle.dump(pw);
     }
 
+    public float[] getLastSensorValues() {
+        return mAmbientLightRingBuffer.getAllLuxValues();
+    }
+
+    public long[] getLastSensorTimestamps() {
+        return mAmbientLightRingBuffer.getAllTimestamps();
+    }
+
     private String configStateToString(int state) {
         switch (state) {
         case AUTO_BRIGHTNESS_ENABLED:
@@ -1231,10 +1239,42 @@
             return mRingLux[offsetOf(index)];
         }
 
+        public float[] getAllLuxValues() {
+            float[] values = new float[mCount];
+            if (mCount == 0) {
+                return values;
+            }
+
+            if (mStart < mEnd) {
+                System.arraycopy(mRingLux, mStart, values, 0, mCount);
+            } else {
+                System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
+                System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
+            }
+
+            return values;
+        }
+
         public long getTime(int index) {
             return mRingTime[offsetOf(index)];
         }
 
+        public long[] getAllTimestamps() {
+            long[] values = new long[mCount];
+            if (mCount == 0) {
+                return values;
+            }
+
+            if (mStart < mEnd) {
+                System.arraycopy(mRingTime, mStart, values, 0, mCount);
+            } else {
+                System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
+                System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
+            }
+
+            return values;
+        }
+
         public void push(long time, float lux) {
             int next = mEnd;
             if (mCount == mCapacity) {
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index df4c471..6e1640d 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -79,10 +79,8 @@
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
-import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Date;
-import java.util.Deque;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -101,8 +99,6 @@
     private static final int MAX_EVENTS = 100;
     // Discard events when reading or writing that are older than this.
     private static final long MAX_EVENT_AGE = TimeUnit.DAYS.toMillis(30);
-    // Time over which we keep lux sensor readings.
-    private static final long LUX_EVENT_HORIZON = TimeUnit.SECONDS.toNanos(10);
 
     private static final String TAG_EVENTS = "events";
     private static final String TAG_EVENT = "event";
@@ -174,8 +170,6 @@
     // Lock held while collecting data related to brightness changes.
     private final Object mDataCollectionLock = new Object();
     @GuardedBy("mDataCollectionLock")
-    private Deque<LightData> mLastSensorReadings = new ArrayDeque<>();
-    @GuardedBy("mDataCollectionLock")
     private float mLastBatteryLevel = Float.NaN;
     @GuardedBy("mDataCollectionLock")
     private float mLastBrightness = -1;
@@ -327,7 +321,8 @@
      */
     public void notifyBrightnessChanged(float brightness, boolean userInitiated,
             float powerBrightnessFactor, boolean isUserSetBrightness,
-            boolean isDefaultBrightnessConfig, String uniqueDisplayId) {
+            boolean isDefaultBrightnessConfig, String uniqueDisplayId, float[] luxValues,
+            long[] luxTimestamps) {
         if (DEBUG) {
             Slog.d(TAG, String.format("notifyBrightnessChanged(brightness=%f, userInitiated=%b)",
                         brightness, userInitiated));
@@ -335,7 +330,7 @@
         Message m = mBgHandler.obtainMessage(MSG_BRIGHTNESS_CHANGED,
                 userInitiated ? 1 : 0, 0 /*unused*/, new BrightnessChangeValues(brightness,
                         powerBrightnessFactor, isUserSetBrightness, isDefaultBrightnessConfig,
-                        mInjector.currentTimeMillis(), uniqueDisplayId));
+                        mInjector.currentTimeMillis(), uniqueDisplayId, luxValues, luxTimestamps));
         m.sendToTarget();
     }
 
@@ -349,7 +344,8 @@
 
     private void handleBrightnessChanged(float brightness, boolean userInitiated,
             float powerBrightnessFactor, boolean isUserSetBrightness,
-            boolean isDefaultBrightnessConfig, long timestamp, String uniqueDisplayId) {
+            boolean isDefaultBrightnessConfig, long timestamp, String uniqueDisplayId,
+            float[] luxValues, long[] luxTimestamps) {
         BrightnessChangeEvent.Builder builder;
 
         synchronized (mDataCollectionLock) {
@@ -376,28 +372,22 @@
             builder.setIsDefaultBrightnessConfig(isDefaultBrightnessConfig);
             builder.setUniqueDisplayId(uniqueDisplayId);
 
-            final int readingCount = mLastSensorReadings.size();
-            if (readingCount == 0) {
+            if (luxValues.length == 0) {
                 // No sensor data so ignore this.
                 return;
             }
 
-            float[] luxValues = new float[readingCount];
-            long[] luxTimestamps = new long[readingCount];
+            long[] luxTimestampsMillis = new long[luxTimestamps.length];
 
-            int pos = 0;
-
-            // Convert sensor timestamp in elapsed time nanos to current time millis.
+            // Convert lux timestamp in elapsed time to current time.
             long currentTimeMillis = mInjector.currentTimeMillis();
             long elapsedTimeNanos = mInjector.elapsedRealtimeNanos();
-            for (LightData reading : mLastSensorReadings) {
-                luxValues[pos] = reading.lux;
-                luxTimestamps[pos] = currentTimeMillis -
-                        TimeUnit.NANOSECONDS.toMillis(elapsedTimeNanos - reading.timestamp);
-                ++pos;
+            for (int i = 0; i < luxTimestamps.length; i++) {
+                luxTimestampsMillis[i] = currentTimeMillis - (TimeUnit.NANOSECONDS.toMillis(
+                        elapsedTimeNanos) - luxTimestamps[i]);
             }
             builder.setLuxValues(luxValues);
-            builder.setLuxTimestamps(luxTimestamps);
+            builder.setLuxTimestamps(luxTimestampsMillis);
 
             builder.setBatteryLevel(mLastBatteryLevel);
             builder.setLastBrightness(previousBrightness);
@@ -452,9 +442,6 @@
         if (mLightSensor != lightSensor) {
             mLightSensor = lightSensor;
             stopSensorListener();
-            synchronized (mDataCollectionLock) {
-                mLastSensorReadings.clear();
-            }
             // Attempt to restart the sensor listener. It will check to see if it should be running
             // so there is no need to also check here.
             startSensorListener();
@@ -798,12 +785,6 @@
             pw.println("  mLightSensor=" + mLightSensor);
             pw.println("  mLastBatteryLevel=" + mLastBatteryLevel);
             pw.println("  mLastBrightness=" + mLastBrightness);
-            pw.println("  mLastSensorReadings.size=" + mLastSensorReadings.size());
-            if (!mLastSensorReadings.isEmpty()) {
-                pw.println("  mLastSensorReadings time span "
-                        + mLastSensorReadings.peekFirst().timestamp + "->"
-                        + mLastSensorReadings.peekLast().timestamp);
-            }
         }
         synchronized (mEventsLock) {
             pw.println("  mEventsDirty=" + mEventsDirty);
@@ -919,43 +900,6 @@
         return ParceledListSlice.emptyList();
     }
 
-    // Not allowed to keep the SensorEvent so used to copy the data we care about.
-    private static class LightData {
-        public float lux;
-        // Time in elapsedRealtimeNanos
-        public long timestamp;
-    }
-
-    private void recordSensorEvent(SensorEvent event) {
-        long horizon = mInjector.elapsedRealtimeNanos() - LUX_EVENT_HORIZON;
-        synchronized (mDataCollectionLock) {
-            if (DEBUG) {
-                Slog.v(TAG, "Sensor event " + event);
-            }
-            if (!mLastSensorReadings.isEmpty()
-                    && event.timestamp < mLastSensorReadings.getLast().timestamp) {
-                // Ignore event that came out of order.
-                return;
-            }
-            LightData data = null;
-            while (!mLastSensorReadings.isEmpty()
-                    && mLastSensorReadings.getFirst().timestamp < horizon) {
-                // Remove data that has fallen out of the window.
-                data = mLastSensorReadings.removeFirst();
-            }
-            // We put back the last one we removed so we know how long
-            // the first sensor reading was valid for.
-            if (data != null) {
-                mLastSensorReadings.addFirst(data);
-            }
-
-            data = new LightData();
-            data.timestamp = event.timestamp;
-            data.lux = event.values[0];
-            mLastSensorReadings.addLast(data);
-        }
-    }
-
     private void recordAmbientBrightnessStats(SensorEvent event) {
         mAmbientBrightnessStatsTracker.add(mCurrentUserId, event.values[0]);
     }
@@ -969,7 +913,6 @@
     private final class SensorListener implements SensorEventListener {
         @Override
         public void onSensorChanged(SensorEvent event) {
-            recordSensorEvent(event);
             recordAmbientBrightnessStats(event);
         }
 
@@ -1056,7 +999,7 @@
                     handleBrightnessChanged(values.brightness, userInitiatedChange,
                             values.powerBrightnessFactor, values.isUserSetBrightness,
                             values.isDefaultBrightnessConfig, values.timestamp,
-                            values.uniqueDisplayId);
+                            values.uniqueDisplayId, values.luxValues, values.luxTimestamps);
                     break;
                 case MSG_START_SENSOR_LISTENER:
                     startSensorListener();
@@ -1092,16 +1035,20 @@
         public final boolean isDefaultBrightnessConfig;
         public final long timestamp;
         public final String uniqueDisplayId;
+        public final float[] luxValues;
+        public final long[] luxTimestamps;
 
         BrightnessChangeValues(float brightness, float powerBrightnessFactor,
                 boolean isUserSetBrightness, boolean isDefaultBrightnessConfig,
-                long timestamp, String uniqueDisplayId) {
+                long timestamp, String uniqueDisplayId, float[] luxValues, long[] luxTimestamps) {
             this.brightness = brightness;
             this.powerBrightnessFactor = powerBrightnessFactor;
             this.isUserSetBrightness = isUserSetBrightness;
             this.isDefaultBrightnessConfig = isDefaultBrightnessConfig;
             this.timestamp = timestamp;
             this.uniqueDisplayId = uniqueDisplayId;
+            this.luxValues = luxValues;
+            this.luxTimestamps = luxTimestamps;
         }
     }
 
diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
index 9dd2f84..fc6403d 100644
--- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
+++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import android.annotation.NonNull;
 import android.hardware.devicestate.DeviceStateManager;
 import android.os.Environment;
 import android.util.IndentingPrintWriter;
@@ -23,8 +24,10 @@
 import android.util.SparseArray;
 import android.view.DisplayAddress;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.config.layout.Layouts;
 import com.android.server.display.config.layout.XmlParser;
+import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
 
 import org.xmlpull.v1.XmlPullParserException;
@@ -48,13 +51,28 @@
 
     public static final int STATE_DEFAULT = DeviceStateManager.INVALID_DEVICE_STATE;
 
+    // Direction of the display relative to the default display, whilst in this state
+    private static final int POSITION_UNKNOWN = Layout.Display.POSITION_UNKNOWN;
+    private static final int POSITION_FRONT = Layout.Display.POSITION_FRONT;
+    private static final int POSITION_REAR = Layout.Display.POSITION_REAR;
+
+    private static final String FRONT_STRING = "front";
+    private static final String REAR_STRING = "rear";
+
     private static final String CONFIG_FILE_PATH =
             "etc/displayconfig/display_layout_configuration.xml";
 
     private final SparseArray<Layout> mLayoutMap = new SparseArray<>();
+    private final DisplayIdProducer mIdProducer;
 
-    DeviceStateToLayoutMap() {
-        loadLayoutsFromConfig();
+    DeviceStateToLayoutMap(DisplayIdProducer idProducer) {
+        this(idProducer, Environment.buildPath(
+                Environment.getVendorDirectory(), CONFIG_FILE_PATH));
+    }
+
+    DeviceStateToLayoutMap(DisplayIdProducer idProducer, File configFile) {
+        mIdProducer = idProducer;
+        loadLayoutsFromConfig(configFile);
         createLayout(STATE_DEFAULT);
     }
 
@@ -76,24 +94,11 @@
         return layout;
     }
 
-    private Layout createLayout(int state) {
-        if (mLayoutMap.contains(state)) {
-            Slog.e(TAG, "Attempted to create a second layout for state " + state);
-            return null;
-        }
-
-        final Layout layout = new Layout();
-        mLayoutMap.append(state, layout);
-        return layout;
-    }
-
     /**
      * Reads display-layout-configuration files to get the layouts to use for this device.
      */
-    private void loadLayoutsFromConfig() {
-        final File configFile = Environment.buildPath(
-                Environment.getVendorDirectory(), CONFIG_FILE_PATH);
-
+    @VisibleForTesting
+    void loadLayoutsFromConfig(@NonNull File configFile) {
         if (!configFile.exists()) {
             return;
         }
@@ -109,10 +114,19 @@
                 final int state = l.getState().intValue();
                 final Layout layout = createLayout(state);
                 for (com.android.server.display.config.layout.Display d: l.getDisplay()) {
-                    layout.createDisplayLocked(
+                    Layout.Display display = layout.createDisplayLocked(
                             DisplayAddress.fromPhysicalDisplayId(d.getAddress().longValue()),
                             d.isDefaultDisplay(),
-                            d.isEnabled());
+                            d.isEnabled(),
+                            mIdProducer);
+
+                    if (FRONT_STRING.equals(d.getPosition())) {
+                        display.setPosition(POSITION_FRONT);
+                    } else if (REAR_STRING.equals(d.getPosition())) {
+                        display.setPosition(POSITION_REAR);
+                    } else {
+                        display.setPosition(POSITION_UNKNOWN);
+                    }
                 }
             }
         } catch (IOException | DatatypeConfigurationException | XmlPullParserException e) {
@@ -120,4 +134,15 @@
                     + configFile, e);
         }
     }
+
+    private Layout createLayout(int state) {
+        if (mLayoutMap.contains(state)) {
+            Slog.e(TAG, "Attempted to create a second layout for state " + state);
+            return null;
+        }
+
+        final Layout layout = new Layout();
+        mLayoutMap.append(state, layout);
+        return layout;
+    }
 }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index 8811999..fe1d1a6 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -145,7 +145,7 @@
     /**
      * Flag: Indicates that the display should always be unlocked. Only valid on virtual displays
      * that aren't in the default display group.
-     * @see #FLAG_OWN_DISPLAY_GROUP
+     * @see #FLAG_OWN_DISPLAY_GROUP and #FLAG_DEVICE_DISPLAY_GROUP
      * @hide
      */
     public static final int FLAG_ALWAYS_UNLOCKED = 1 << 15;
@@ -172,6 +172,14 @@
     public static final int FLAG_OWN_FOCUS = 1 << 17;
 
     /**
+     * Flag: indicates that the display should not be a part of the default {@link DisplayGroup} and
+     * instead be part of a {@link DisplayGroup} associated with the Virtual Device.
+     *
+     * @hide
+     */
+    public static final int FLAG_DEVICE_DISPLAY_GROUP = 1 << 18;
+
+    /**
      * Touch attachment: Display does not receive touch.
      */
     public static final int TOUCH_NONE = 0;
@@ -237,6 +245,12 @@
     public int modeId;
 
     /**
+     * The render frame rate this display is scheduled at.
+     * @see android.view.DisplayInfo#renderFrameRate for more details.
+     */
+    public float renderFrameRate;
+
+    /**
      * The default mode of the display.
      */
     public int defaultModeId;
@@ -431,6 +445,7 @@
                 || width != other.width
                 || height != other.height
                 || modeId != other.modeId
+                || renderFrameRate != other.renderFrameRate
                 || defaultModeId != other.defaultModeId
                 || !Arrays.equals(supportedModes, other.supportedModes)
                 || !Arrays.equals(supportedColorModes, other.supportedColorModes)
@@ -475,6 +490,7 @@
         width = other.width;
         height = other.height;
         modeId = other.modeId;
+        renderFrameRate = other.renderFrameRate;
         defaultModeId = other.defaultModeId;
         supportedModes = other.supportedModes;
         colorMode = other.colorMode;
@@ -515,6 +531,7 @@
         sb.append(name).append("\": uniqueId=\"").append(uniqueId).append("\", ");
         sb.append(width).append(" x ").append(height);
         sb.append(", modeId ").append(modeId);
+        sb.append(", renderFrameRate ").append(renderFrameRate);
         sb.append(", defaultModeId ").append(defaultModeId);
         sb.append(", supportedModes ").append(Arrays.toString(supportedModes));
         sb.append(", colorMode ").append(colorMode);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index a7e6b7f..ae84e96 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -25,6 +25,7 @@
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
@@ -35,6 +36,7 @@
 import static android.hardware.display.DisplayViewport.VIEWPORT_EXTERNAL;
 import static android.hardware.display.DisplayViewport.VIEWPORT_INTERNAL;
 import static android.hardware.display.DisplayViewport.VIEWPORT_VIRTUAL;
+import static android.os.Process.FIRST_APPLICATION_UID;
 import static android.os.Process.ROOT_UID;
 
 import android.Manifest;
@@ -881,20 +883,27 @@
 
     private DisplayInfo getDisplayInfoForFrameRateOverride(DisplayEventReceiver.FrameRateOverride[]
             frameRateOverrides, DisplayInfo info, int callingUid) {
-        float frameRateHz = 0;
+        float frameRateHz = info.renderFrameRate;
         for (DisplayEventReceiver.FrameRateOverride frameRateOverride : frameRateOverrides) {
             if (frameRateOverride.uid == callingUid) {
                 frameRateHz = frameRateOverride.frameRateHz;
                 break;
             }
         }
+
         if (frameRateHz == 0) {
             return info;
         }
 
+        // For non-apps users we always return the physical refresh rate from display mode
+        boolean displayModeReturnsPhysicalRefreshRate =
+                callingUid < FIRST_APPLICATION_UID
+                        || CompatChanges.isChangeEnabled(
+                                DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE, callingUid);
+
         // Override the refresh rate only if it is a divisor of the current
         // refresh rate. This calculation needs to be in sync with the native code
-        // in RefreshRateConfigs::getFrameRateDivisor
+        // in RefreshRateSelector::getFrameRateDivisor
         Display.Mode currentMode = info.getMode();
         float numPeriods = currentMode.getRefreshRate() / frameRateHz;
         float numPeriodsRound = Math.round(numPeriods);
@@ -918,8 +927,7 @@
                 }
                 overriddenInfo.refreshRateOverride = mode.getRefreshRate();
 
-                if (!CompatChanges.isChangeEnabled(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE,
-                        callingUid)) {
+                if (!displayModeReturnsPhysicalRefreshRate) {
                     overriddenInfo.modeId = mode.getModeId();
                 }
                 return overriddenInfo;
@@ -927,8 +935,7 @@
         }
 
         overriddenInfo.refreshRateOverride = frameRateHz;
-        if (!CompatChanges.isChangeEnabled(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE,
-                callingUid)) {
+        if (!displayModeReturnsPhysicalRefreshRate) {
             overriddenInfo.supportedModes = Arrays.copyOf(info.supportedModes,
                     info.supportedModes.length + 1);
             overriddenInfo.supportedModes[overriddenInfo.supportedModes.length - 1] =
@@ -1276,6 +1283,9 @@
         if ((flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
             flags &= ~VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
         }
+        if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0 && virtualDevice != null) {
+            flags |= VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
+        }
 
         if (projection != null) {
             try {
@@ -1403,7 +1413,7 @@
         // If the display is to be added to a device display group, we need to make the
         // LogicalDisplayMapper aware of the link between the new display and its associated virtual
         // device before triggering DISPLAY_DEVICE_EVENT_ADDED.
-        if (virtualDevice != null && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0) {
+        if ((flags & VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP) != 0) {
             try {
                 final int virtualDeviceId = virtualDevice.getDeviceId();
                 mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index de26abc..9d47892 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -2435,7 +2435,9 @@
                     : 1.0f;
             mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated,
                     powerFactor, hadUserDataPoint,
-                    mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId);
+                    mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId,
+                    mAutomaticBrightnessController.getLastSensorValues(),
+                    mAutomaticBrightnessController.getLastSensorTimestamps());
         }
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 1f9df9e..346b340 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -291,7 +291,6 @@
     private boolean mAppliedDimming;
     private boolean mAppliedLowPower;
     private boolean mAppliedTemporaryAutoBrightnessAdjustment;
-    private boolean mAppliedBrightnessBoost;
     private boolean mAppliedThrottling;
 
     // Reason for which the brightness was last changed. See {@link BrightnessReason} for more
@@ -1202,18 +1201,6 @@
             brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO;
             mAppliedTemporaryAutoBrightnessAdjustment = false;
         }
-        // Apply brightness boost.
-        // We do this here after deciding whether auto-brightness is enabled so that we don't
-        // disable the light sensor during this temporary state.  That way when boost ends we will
-        // be able to resume normal auto-brightness behavior without any delay.
-        if (mPowerRequest.boostScreenBrightness
-                && brightnessState != PowerManager.BRIGHTNESS_OFF_FLOAT) {
-            brightnessState = PowerManager.BRIGHTNESS_MAX;
-            mBrightnessReasonTemp.setReason(BrightnessReason.REASON_BOOST);
-            mAppliedBrightnessBoost = true;
-        } else {
-            mAppliedBrightnessBoost = false;
-        }
 
         // If the brightness is already set then it's been overridden by something other than the
         // user, or is a temporary adjustment.
@@ -2154,7 +2141,9 @@
                     : 1.0f;
             mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated,
                     powerFactor, hadUserDataPoint,
-                    mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId);
+                    mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId,
+                    mAutomaticBrightnessController.getLastSensorValues(),
+                    mAutomaticBrightnessController.getLastSensorTimestamps());
         }
     }
 
@@ -2227,7 +2216,6 @@
         pw.println("  mAppliedThrottling=" + mAppliedThrottling);
         pw.println("  mAppliedTemporaryAutoBrightnessAdjustment="
                 + mAppliedTemporaryAutoBrightnessAdjustment);
-        pw.println("  mAppliedBrightnessBoost=" + mAppliedBrightnessBoost);
         pw.println("  mDozing=" + mDozing);
         pw.println("  mSkipRampState=" + skipRampStateToString(mSkipRampState));
         pw.println("  mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index dc5c80f2..ee53b60 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -226,6 +226,8 @@
         private SurfaceControl.DisplayMode[] mSfDisplayModes;
         // The active display mode in SurfaceFlinger
         private SurfaceControl.DisplayMode mActiveSfDisplayMode;
+        // The active display vsync period in SurfaceFlinger
+        private float mActiveRenderFrameRate;
 
         private DisplayEventReceiver.FrameRateOverride[] mFrameRateOverrides =
                 new DisplayEventReceiver.FrameRateOverride[0];
@@ -267,7 +269,7 @@
                 SurfaceControl.DesiredDisplayModeSpecs modeSpecs) {
             boolean changed = updateDisplayModesLocked(
                     dynamicInfo.supportedDisplayModes, dynamicInfo.preferredBootDisplayMode,
-                    dynamicInfo.activeDisplayModeId, modeSpecs);
+                    dynamicInfo.activeDisplayModeId, dynamicInfo.renderFrameRate, modeSpecs);
             changed |= updateStaticInfo(staticInfo);
             changed |= updateColorModesLocked(dynamicInfo.supportedColorModes,
                     dynamicInfo.activeColorMode);
@@ -283,7 +285,8 @@
 
         public boolean updateDisplayModesLocked(
                 SurfaceControl.DisplayMode[] displayModes, int preferredSfDisplayModeId,
-                int activeSfDisplayModeId, SurfaceControl.DesiredDisplayModeSpecs modeSpecs) {
+                int activeSfDisplayModeId, float renderFrameRate,
+                SurfaceControl.DesiredDisplayModeSpecs modeSpecs) {
             mSfDisplayModes = Arrays.copyOf(displayModes, displayModes.length);
             mActiveSfDisplayMode = getModeById(displayModes, activeSfDisplayModeId);
             SurfaceControl.DisplayMode preferredSfDisplayMode =
@@ -379,6 +382,16 @@
                 sendTraversalRequestLocked();
             }
 
+            boolean renderFrameRateChanged = false;
+
+            if (mActiveRenderFrameRate > 0 &&  mActiveRenderFrameRate != renderFrameRate) {
+                Slog.d(TAG, "The render frame rate was changed from SurfaceFlinger or the display"
+                        + " device to " + renderFrameRate);
+                mActiveRenderFrameRate = renderFrameRate;
+                renderFrameRateChanged = true;
+                sendTraversalRequestLocked();
+            }
+
             // Check whether surface flinger spontaneously changed display config specs out from
             // under us. If so, schedule a traversal to reapply our display config specs.
             if (mDisplayModeSpecs.baseModeId != INVALID_MODE_ID) {
@@ -398,7 +411,7 @@
             boolean recordsChanged = records.size() != mSupportedModes.size() || modesAdded;
             // If the records haven't changed then we're done here.
             if (!recordsChanged) {
-                return activeModeChanged || preferredModeChanged;
+                return activeModeChanged || preferredModeChanged || renderFrameRateChanged;
             }
 
             mSupportedModes.clear();
@@ -410,16 +423,19 @@
             if (mDefaultModeId == INVALID_MODE_ID) {
                 mDefaultModeId = activeRecord.mMode.getModeId();
                 mDefaultModeGroup = mActiveSfDisplayMode.group;
+                mActiveRenderFrameRate = renderFrameRate;
             } else if (modesAdded && activeModeChanged) {
                 Slog.d(TAG, "New display modes are added and the active mode has changed, "
                         + "use active mode as default mode.");
                 mDefaultModeId = activeRecord.mMode.getModeId();
                 mDefaultModeGroup = mActiveSfDisplayMode.group;
+                mActiveRenderFrameRate = renderFrameRate;
             } else if (findSfDisplayModeIdLocked(mDefaultModeId, mDefaultModeGroup) < 0) {
                 Slog.w(TAG, "Default display mode no longer available, using currently"
                         + " active mode as default.");
                 mDefaultModeId = activeRecord.mMode.getModeId();
                 mDefaultModeGroup = mActiveSfDisplayMode.group;
+                mActiveRenderFrameRate = renderFrameRate;
             }
 
             // Determine whether the display mode specs' base mode is still there.
@@ -620,6 +636,7 @@
                 mInfo.width = mActiveSfDisplayMode.width;
                 mInfo.height = mActiveSfDisplayMode.height;
                 mInfo.modeId = mActiveModeId;
+                mInfo.renderFrameRate = mActiveRenderFrameRate;
                 mInfo.defaultModeId = getPreferredModeId();
                 mInfo.supportedModes = getDisplayModes(mSupportedModes);
                 mInfo.colorMode = mActiveColorMode;
@@ -995,8 +1012,8 @@
             updateDeviceInfoLocked();
         }
 
-        public void onActiveDisplayModeChangedLocked(int sfModeId) {
-            if (updateActiveModeLocked(sfModeId)) {
+        public void onActiveDisplayModeChangedLocked(int sfModeId, float renderFrameRate) {
+            if (updateActiveModeLocked(sfModeId, renderFrameRate)) {
                 updateDeviceInfoLocked();
             }
         }
@@ -1008,8 +1025,9 @@
             }
         }
 
-        public boolean updateActiveModeLocked(int activeSfModeId) {
-            if (mActiveSfDisplayMode.id == activeSfModeId) {
+        public boolean updateActiveModeLocked(int activeSfModeId, float renderFrameRate) {
+            if (mActiveSfDisplayMode.id == activeSfModeId
+                    && mActiveRenderFrameRate == renderFrameRate) {
                 return false;
             }
             mActiveSfDisplayMode = getModeById(mSfDisplayModes, activeSfModeId);
@@ -1018,6 +1036,7 @@
                 Slog.w(TAG, "In unknown mode after setting allowed modes"
                         + ", activeModeId=" + activeSfModeId);
             }
+            mActiveRenderFrameRate = renderFrameRate;
             return true;
         }
 
@@ -1114,6 +1133,7 @@
                 pw.println("  " + sfDisplayMode);
             }
             pw.println("mActiveSfDisplayMode=" + mActiveSfDisplayMode);
+            pw.println("mActiveRenderFrameRate=" + mActiveRenderFrameRate);
             pw.println("mSupportedModes=");
             for (int i = 0; i < mSupportedModes.size(); i++) {
                 pw.println("  " + mSupportedModes.valueAt(i));
@@ -1288,7 +1308,8 @@
 
     public interface DisplayEventListener {
         void onHotplug(long timestampNanos, long physicalDisplayId, boolean connected);
-        void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId);
+        void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+                long renderPeriod);
         void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
                 DisplayEventReceiver.FrameRateOverride[] overrides);
 
@@ -1309,8 +1330,9 @@
         }
 
         @Override
-        public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
-            mListener.onModeChanged(timestampNanos, physicalDisplayId, modeId);
+        public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+                long renderPeriod) {
+            mListener.onModeChanged(timestampNanos, physicalDisplayId, modeId, renderPeriod);
         }
 
         @Override
@@ -1333,12 +1355,14 @@
         }
 
         @Override
-        public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
+        public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
+                long renderPeriod) {
             if (DEBUG) {
                 Slog.d(TAG, "onModeChanged("
                         + "timestampNanos=" + timestampNanos
                         + ", physicalDisplayId=" + physicalDisplayId
-                        + ", modeId=" + modeId + ")");
+                        + ", modeId=" + modeId
+                        + ", renderPeriod=" + renderPeriod + ")");
             }
             synchronized (getSyncRoot()) {
                 LocalDisplayDevice device = mDevices.get(physicalDisplayId);
@@ -1349,7 +1373,8 @@
                     }
                     return;
                 }
-                device.onActiveDisplayModeChangedLocked(modeId);
+                float renderFrameRate = 1e9f / renderPeriod;
+                device.onActiveDisplayModeChangedLocked(modeId, renderFrameRate);
             }
         }
 
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 8dd169bf..c7b27de 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -377,6 +377,7 @@
             mBaseDisplayInfo.logicalHeight = maskedHeight;
             mBaseDisplayInfo.rotation = Surface.ROTATION_0;
             mBaseDisplayInfo.modeId = deviceInfo.modeId;
+            mBaseDisplayInfo.renderFrameRate = deviceInfo.renderFrameRate;
             mBaseDisplayInfo.defaultModeId = deviceInfo.defaultModeId;
             mBaseDisplayInfo.supportedModes = Arrays.copyOf(
                     deviceInfo.supportedModes, deviceInfo.supportedModes.length);
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 66073c2..80f47a1 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -40,6 +40,7 @@
 import android.view.DisplayInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
 
 import java.io.PrintWriter;
@@ -82,6 +83,8 @@
     private static final int UPDATE_STATE_TRANSITION = 1;
     private static final int UPDATE_STATE_UPDATED = 2;
 
+    private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1;
+
     /**
      * Temporary display info, used for comparing display configurations.
      */
@@ -170,6 +173,8 @@
     private final ArrayMap<String, Integer> mVirtualDeviceDisplayMapping = new ArrayMap<>();
 
     private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
+    private final DisplayIdProducer mIdProducer = (isDefault) ->
+            isDefault ? DEFAULT_DISPLAY : sNextNonDefaultDisplayId++;
     private Layout mCurrentLayout = null;
     private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
     private int mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
@@ -179,7 +184,9 @@
     LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo,
             @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
             @NonNull Handler handler) {
-        this(context, repo, listener, syncRoot, handler, new DeviceStateToLayoutMap());
+        this(context, repo, listener, syncRoot, handler,
+                new DeviceStateToLayoutMap((isDefault) -> isDefault ? DEFAULT_DISPLAY
+                        : sNextNonDefaultDisplayId++));
     }
 
     LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo,
@@ -588,7 +595,7 @@
 
         // Create a logical display for the new display device
         LogicalDisplay display = createNewLogicalDisplayLocked(
-                device, Layout.assignDisplayIdLocked(false /*isDefault*/));
+                device, mIdProducer.getId(/* isDefault= */ false));
 
         applyLayoutLocked();
         updateLogicalDisplaysLocked();
@@ -621,7 +628,7 @@
                         & DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY) != 0
                         && !nextDeviceInfo.address.equals(deviceInfo.address)) {
                     layout.createDisplayLocked(nextDeviceInfo.address,
-                            /* isDefault= */ true, /* isEnabled= */ true);
+                            /* isDefault= */ true, /* isEnabled= */ true, mIdProducer);
                     applyLayoutLocked();
                     return;
                 }
@@ -1036,7 +1043,8 @@
             return;
         }
         final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
-        layout.createDisplayLocked(info.address, /* isDefault= */ true, /* isEnabled= */ true);
+        layout.createDisplayLocked(info.address, /* isDefault= */ true, /* isEnabled= */ true,
+                mIdProducer);
     }
 
     private int assignLayerStackLocked(int displayId) {
diff --git a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
index 0e11b53..3e67f0a 100644
--- a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
@@ -340,6 +340,7 @@
                 mInfo.width = mode.getPhysicalWidth();
                 mInfo.height = mode.getPhysicalHeight();
                 mInfo.modeId = mode.getModeId();
+                mInfo.renderFrameRate = mode.getRefreshRate();
                 mInfo.defaultModeId = mModes[0].getModeId();
                 mInfo.supportedModes = mModes;
                 mInfo.densityDpi = rawMode.mDensityDpi;
diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index f30a84f..e7601bc 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -619,7 +619,7 @@
 
     private static final class DisplayState {
         private int mColorMode;
-        private float mBrightness;
+        private float mBrightness = Float.NaN;
         private int mWidth;
         private int mHeight;
         private float mRefreshRate;
@@ -700,7 +700,11 @@
                         break;
                     case TAG_BRIGHTNESS_VALUE:
                         String brightness = parser.nextText();
-                        mBrightness = Float.parseFloat(brightness);
+                        try {
+                            mBrightness = Float.parseFloat(brightness);
+                        } catch (NumberFormatException e) {
+                            mBrightness = Float.NaN;
+                        }
                         break;
                     case TAG_BRIGHTNESS_CONFIGURATIONS:
                         mDisplayBrightnessConfigurations.loadFromXml(parser);
@@ -727,7 +731,9 @@
             serializer.endTag(null, TAG_COLOR_MODE);
 
             serializer.startTag(null, TAG_BRIGHTNESS_VALUE);
-            serializer.text(Float.toString(mBrightness));
+            if (!Float.isNaN(mBrightness)) {
+                serializer.text(Float.toString(mBrightness));
+            }
             serializer.endTag(null, TAG_BRIGHTNESS_VALUE);
 
             serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index d0e518b..a118b2f 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -20,6 +20,7 @@
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_FOCUS;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
@@ -32,6 +33,7 @@
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
 
 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_TOUCH_FEEDBACK_DISABLED;
 import static com.android.server.display.DisplayDeviceInfo.FLAG_TRUSTED;
@@ -446,6 +448,7 @@
                 mInfo.width = mWidth;
                 mInfo.height = mHeight;
                 mInfo.modeId = mMode.getModeId();
+                mInfo.renderFrameRate = mMode.getRefreshRate();
                 mInfo.defaultModeId = mMode.getModeId();
                 mInfo.supportedModes = new Display.Mode[] { mMode };
                 mInfo.densityDpi = mDensityDpi;
@@ -466,6 +469,9 @@
                         mInfo.flags |= FLAG_OWN_DISPLAY_GROUP;
                     }
                 }
+                if ((mFlags & VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP) != 0) {
+                    mInfo.flags |= FLAG_DEVICE_DISPLAY_GROUP;
+                }
 
                 if ((mFlags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0) {
                     mInfo.flags |= DisplayDeviceInfo.FLAG_SECURE;
@@ -498,11 +504,15 @@
                     mInfo.flags |= FLAG_TRUSTED;
                 }
                 if ((mFlags & VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED) != 0) {
-                    if ((mFlags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) {
+                    if ((mInfo.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP) != 0
+                            || (mFlags & VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP) != 0) {
                         mInfo.flags |= FLAG_ALWAYS_UNLOCKED;
                     } else {
-                        Slog.w(TAG, "Ignoring VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED as it "
-                                + "requires VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP.");
+                        Slog.w(
+                                TAG,
+                                "Ignoring VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED as it requires"
+                                    + " VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP or"
+                                    + " VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP.");
                     }
                 }
                 if ((mFlags & VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED) != 0) {
diff --git a/services/core/java/com/android/server/display/WifiDisplayAdapter.java b/services/core/java/com/android/server/display/WifiDisplayAdapter.java
index c759d98..e832701 100644
--- a/services/core/java/com/android/server/display/WifiDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/WifiDisplayAdapter.java
@@ -646,6 +646,7 @@
                 mInfo.width = mWidth;
                 mInfo.height = mHeight;
                 mInfo.modeId = mMode.getModeId();
+                mInfo.renderFrameRate = mMode.getRefreshRate();
                 mInfo.defaultModeId = mMode.getModeId();
                 mInfo.supportedModes = new Display.Mode[] { mMode };
                 mInfo.presentationDeadlineNanos = 1000000000L / (int) mRefreshRate; // 1 frame
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
index 4759b7d..7d05f13 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
@@ -25,6 +25,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.brightness.strategy.BoostBrightnessStrategy;
 import com.android.server.display.brightness.strategy.DisplayBrightnessStrategy;
 import com.android.server.display.brightness.strategy.DozeBrightnessStrategy;
 import com.android.server.display.brightness.strategy.InvalidBrightnessStrategy;
@@ -52,6 +53,8 @@
     private final OverrideBrightnessStrategy mOverrideBrightnessStrategy;
     // The brightness strategy used to manage the brightness state in temporary state
     private final TemporaryBrightnessStrategy mTemporaryBrightnessStrategy;
+    // The brightness strategy used to manage the brightness state when boost is requested
+    private final BoostBrightnessStrategy mBoostBrightnessStrategy;
     // The brightness strategy used to manage the brightness state when the request is invalid.
     private final InvalidBrightnessStrategy mInvalidBrightnessStrategy;
 
@@ -72,6 +75,7 @@
         mScreenOffBrightnessStrategy = injector.getScreenOffBrightnessStrategy();
         mOverrideBrightnessStrategy = injector.getOverrideBrightnessStrategy();
         mTemporaryBrightnessStrategy = injector.getTemporaryBrightnessStrategy();
+        mBoostBrightnessStrategy = injector.getBoostBrightnessStrategy();
         mInvalidBrightnessStrategy = injector.getInvalidBrightnessStrategy();
         mAllowAutoBrightnessWhileDozingConfig = context.getResources().getBoolean(
                 R.bool.config_allowAutoBrightnessWhileDozing);
@@ -89,6 +93,8 @@
         DisplayBrightnessStrategy displayBrightnessStrategy = mInvalidBrightnessStrategy;
         if (targetDisplayState == Display.STATE_OFF) {
             displayBrightnessStrategy = mScreenOffBrightnessStrategy;
+        } else if (displayPowerRequest.boostScreenBrightness) {
+            displayBrightnessStrategy = mBoostBrightnessStrategy;
         } else if (shouldUseDozeBrightnessStrategy(displayPowerRequest)) {
             displayBrightnessStrategy = mDozeBrightnessStrategy;
         } else if (BrightnessUtils
@@ -170,6 +176,10 @@
             return new TemporaryBrightnessStrategy();
         }
 
+        BoostBrightnessStrategy getBoostBrightnessStrategy() {
+            return new BoostBrightnessStrategy();
+        }
+
         InvalidBrightnessStrategy getInvalidBrightnessStrategy() {
             return new InvalidBrightnessStrategy();
         }
diff --git a/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java
new file mode 100644
index 0000000..475ef50
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.brightness.strategy;
+
+import android.hardware.display.DisplayManagerInternal;
+import android.os.PowerManager;
+
+import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.brightness.BrightnessUtils;
+
+/**
+ * Manages the brightness of the display when the system brightness boost is requested.
+ */
+public class BoostBrightnessStrategy implements DisplayBrightnessStrategy {
+
+    public BoostBrightnessStrategy() {
+    }
+
+    // Set the brightness to the maximum value when display brightness boost is requested
+    @Override
+    public DisplayBrightnessState updateBrightness(
+            DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) {
+        // Todo(brup): Introduce a validator class and add validations before setting the brightness
+        DisplayBrightnessState displayBrightnessState =
+                BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_BOOST,
+                        PowerManager.BRIGHTNESS_MAX,
+                        PowerManager.BRIGHTNESS_MAX);
+        return displayBrightnessState;
+    }
+
+    @Override
+    public String getName() {
+        return "BoostBrightnessStrategy";
+    }
+}
diff --git a/core/java/android/window/BackEvent.aidl b/services/core/java/com/android/server/display/layout/DisplayIdProducer.java
similarity index 64%
copy from core/java/android/window/BackEvent.aidl
copy to services/core/java/com/android/server/display/layout/DisplayIdProducer.java
index 821f1fa..3029757 100644
--- a/core/java/android/window/BackEvent.aidl
+++ b/services/core/java/com/android/server/display/layout/DisplayIdProducer.java
@@ -14,9 +14,17 @@
  * limitations under the License.
  */
 
-package android.window;
+package com.android.server.display.layout;
 
 /**
- * @hide
+ * Interface for producing logical display ids.
  */
-parcelable BackEvent;
+public interface DisplayIdProducer {
+
+    /**
+     * Generates a new display ID
+     * @param isDefault if requested display is the default display.
+     * @return the next unique logical display Id.
+     */
+    int getId(boolean isDefault);
+}
diff --git a/services/core/java/com/android/server/display/layout/Layout.java b/services/core/java/com/android/server/display/layout/Layout.java
index 7e16ea8..4a466fd 100644
--- a/services/core/java/com/android/server/display/layout/Layout.java
+++ b/services/core/java/com/android/server/display/layout/Layout.java
@@ -50,15 +50,33 @@
         return mDisplays.toString();
     }
 
+    @Override
+    public boolean equals(Object obj) {
+
+        if (!(obj instanceof  Layout)) {
+            return false;
+        }
+
+        Layout otherLayout = (Layout) obj;
+        return this.mDisplays.equals(otherLayout.mDisplays);
+    }
+
+    @Override
+    public int hashCode() {
+        return mDisplays.hashCode();
+    }
+
     /**
      * Creates a simple 1:1 LogicalDisplay mapping for the specified DisplayDevice.
      *
      * @param address Address of the device.
      * @param isDefault Indicates if the device is meant to be the default display.
+     * @param isEnabled Indicates if this display is usable and can be switched on
      * @return The new layout.
      */
     public Display createDisplayLocked(
-            @NonNull DisplayAddress address, boolean isDefault, boolean isEnabled) {
+            @NonNull DisplayAddress address, boolean isDefault, boolean isEnabled,
+            DisplayIdProducer idProducer) {
         if (contains(address)) {
             Slog.w(TAG, "Attempting to add second definition for display-device: " + address);
             return null;
@@ -74,7 +92,7 @@
         // Note that the logical display ID is saved into the layout, so when switching between
         // different layouts, a logical display can be destroyed and later recreated with the
         // same logical display ID.
-        final int logicalDisplayId = assignDisplayIdLocked(isDefault);
+        final int logicalDisplayId = idProducer.getId(isDefault);
         final Display display = new Display(address, logicalDisplayId, isEnabled);
 
         mDisplays.add(display);
@@ -158,25 +176,64 @@
      * Describes how a {@link LogicalDisplay} is built from {@link DisplayDevice}s.
      */
     public static class Display {
+        public static final int POSITION_UNKNOWN = -1;
+        public static final int POSITION_FRONT = 0;
+        public static final int POSITION_REAR = 1;
+
         // Address of the display device to map to this display.
         private final DisplayAddress mAddress;
 
         // Logical Display ID to apply to this display.
         private final int mLogicalDisplayId;
 
-        // Indicates that this display is not usable and should remain off.
+        // Indicates if this display is usable and can be switched on
         private final boolean mIsEnabled;
 
+        // The direction the display faces
+        // {@link DeviceStateToLayoutMap.POSITION_FRONT} or
+        // {@link DeviceStateToLayoutMap.POSITION_REAR}.
+        // {@link DeviceStateToLayoutMap.POSITION_UNKNOWN} is unspecified.
+        private int mPosition;
+
         Display(@NonNull DisplayAddress address, int logicalDisplayId, boolean isEnabled) {
             mAddress = address;
             mLogicalDisplayId = logicalDisplayId;
             mIsEnabled = isEnabled;
+            mPosition = POSITION_UNKNOWN;
         }
 
         @Override
         public String toString() {
-            return "{addr: " + mAddress + ", dispId: " + mLogicalDisplayId
-                    + "(" + (mIsEnabled ? "ON" : "OFF") + ")}";
+            return "{"
+                    + "dispId: " + mLogicalDisplayId
+                    + "(" + (mIsEnabled ? "ON" : "OFF") + ")"
+                    + ", addr: " + mAddress
+                    +  ((mPosition == POSITION_UNKNOWN) ? "" : ", position: " + mPosition)
+                    + "}";
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof Display)) {
+                return false;
+            }
+
+            Display otherDisplay = (Display) obj;
+
+            return otherDisplay.mIsEnabled == this.mIsEnabled
+                    && otherDisplay.mPosition == this.mPosition
+                    && otherDisplay.mLogicalDisplayId == this.mLogicalDisplayId
+                    && this.mAddress.equals(otherDisplay.mAddress);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = 1;
+            result = 31 * result + Boolean.hashCode(mIsEnabled);
+            result = 31 * result + mPosition;
+            result = 31 * result + mLogicalDisplayId;
+            result = 31 * result + mAddress.hashCode();
+            return result;
         }
 
         public DisplayAddress getAddress() {
@@ -190,5 +247,9 @@
         public boolean isEnabled() {
             return mIsEnabled;
         }
+
+        public void setPosition(int position) {
+            mPosition = position;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java b/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
index 4855be6..ccb2633 100644
--- a/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
+++ b/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.hdmi;
 
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 
 /**
@@ -33,6 +35,10 @@
         super(source);
     }
 
+    ArcTerminationActionFromAvr(HdmiCecLocalDevice source, IHdmiControlCallback callback) {
+        super(source, callback);
+    }
+
     @Override
     boolean start() {
         mState = STATE_WAITING_FOR_INITIATE_ARC_RESPONSE;
@@ -47,10 +53,19 @@
             return false;
         }
         switch (cmd.getOpcode()) {
+            case Constants.MESSAGE_FEATURE_ABORT:
+                int originalOpcode = cmd.getParams()[0] & 0xFF;
+                if (originalOpcode == Constants.MESSAGE_TERMINATE_ARC) {
+                    mState = STATE_ARC_TERMINATED;
+                    audioSystem().processArcTermination();
+                    finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+                    return true;
+                }
+                return false;
             case Constants.MESSAGE_REPORT_ARC_TERMINATED:
                 mState = STATE_ARC_TERMINATED;
                 audioSystem().processArcTermination();
-                finish();
+                finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
                 return true;
         }
         return false;
@@ -79,7 +94,7 @@
                         audioSystem().setArcStatus(false);
                     }
                     HdmiLogger.debug("Terminate ARC was not successfully sent.");
-                    finish();
+                    finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
                 }
             });
     }
@@ -88,6 +103,6 @@
         // Disable ARC if TV didn't respond with <Report ARC Terminated> in time.
         audioSystem().setArcStatus(false);
         HdmiLogger.debug("handleTerminateArcTimeout");
-        finish();
+        finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
     }
 }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index ccaa9255d..a026c4b 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -458,8 +458,16 @@
             HdmiLogger.debug("ARC is not established between TV and AVR device");
             return Constants.ABORT_NOT_IN_CORRECT_MODE;
         } else {
-            removeAction(ArcTerminationActionFromAvr.class);
-            addAndStartAction(new ArcTerminationActionFromAvr(this));
+            if (!getActions(ArcTerminationActionFromAvr.class).isEmpty()
+                    && !getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.isEmpty()) {
+                IHdmiControlCallback callback =
+                        getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0);
+                removeAction(ArcTerminationActionFromAvr.class);
+                addAndStartAction(new ArcTerminationActionFromAvr(this, callback));
+            } else {
+                removeAction(ArcTerminationActionFromAvr.class);
+                addAndStartAction(new ArcTerminationActionFromAvr(this));
+            }
             return Constants.HANDLED;
         }
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 43cd71a..2f15e57 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -878,16 +878,30 @@
             Slog.w(TAG, "Device type doesn't support ARC.");
             return;
         }
+        boolean isArcEnabled = false;
         if (settingValue == SOUNDBAR_MODE_DISABLED && audioSystem != null) {
-            if (audioSystem.isArcEnabled()) {
-                audioSystem.addAndStartAction(new ArcTerminationActionFromAvr(audioSystem));
-            }
+            isArcEnabled = audioSystem.isArcEnabled();
             if (isSystemAudioActivated()) {
                 audioSystem.terminateSystemAudioMode();
             }
+            if (isArcEnabled) {
+                if (audioSystem.hasAction(ArcTerminationActionFromAvr.class)) {
+                    audioSystem.removeAction(ArcTerminationActionFromAvr.class);
+                }
+                audioSystem.addAndStartAction(new ArcTerminationActionFromAvr(audioSystem,
+                        new IHdmiControlCallback.Stub() {
+                            @Override
+                            public void onComplete(int result) {
+                                mAddressAllocated = false;
+                                initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
+                            }
+                        }));
+            }
         }
-        mAddressAllocated = false;
-        initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
+        if (!isArcEnabled) {
+            mAddressAllocated = false;
+            initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 199519c..81d782e 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -98,6 +98,7 @@
 import android.view.SurfaceControl;
 import android.view.VerifiedInputEvent;
 import android.view.ViewConfiguration;
+import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.R;
@@ -1184,6 +1185,33 @@
                 keyboardLayoutDescriptor);
     }
 
+    @Override // Binder call
+    public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype) {
+        return mKeyboardLayoutManager.getKeyboardLayoutForInputDevice(identifier, userId,
+                imeInfo, imeSubtype);
+    }
+
+    @EnforcePermission(Manifest.permission.SET_KEYBOARD_LAYOUT)
+    @Override // Binder call
+    public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype, String keyboardLayoutDescriptor) {
+        super.setKeyboardLayoutForInputDevice_enforcePermission();
+        mKeyboardLayoutManager.setKeyboardLayoutForInputDevice(identifier, userId, imeInfo,
+                imeSubtype, keyboardLayoutDescriptor);
+    }
+
+    @Override // Binder call
+    public String[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype) {
+        return mKeyboardLayoutManager.getKeyboardLayoutListForInputDevice(identifier, userId,
+                imeInfo, imeSubtype);
+    }
+
+
     public void switchKeyboardLayout(int deviceId, int direction) {
         mKeyboardLayoutManager.switchKeyboardLayout(deviceId, direction);
     }
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index c2157a6..1bb14aa 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -17,6 +17,7 @@
 package com.android.server.input;
 
 import android.annotation.NonNull;
+import android.annotation.UserIdInt;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -46,6 +47,8 @@
 import android.util.Log;
 import android.util.Slog;
 import android.view.InputDevice;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
 import android.widget.Toast;
 
 import com.android.internal.R;
@@ -142,7 +145,7 @@
     @Override
     public void onInputDeviceChanged(int deviceId) {
         final InputDevice inputDevice = getInputDevice(deviceId);
-        if (inputDevice == null) {
+        if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
             return;
         }
         synchronized (mDataStore) {
@@ -545,6 +548,35 @@
         }
     }
 
+    public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype) {
+        // TODO(b/259530132): Implement the new keyboard layout API: Returning non-IME specific
+        //  layout for now.
+        return getCurrentKeyboardLayoutForInputDevice(identifier);
+    }
+
+    public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype, String keyboardLayoutDescriptor) {
+        // TODO(b/259530132): Implement the new keyboard layout API: setting non-IME specific
+        //  layout for now.
+        setCurrentKeyboardLayoutForInputDevice(identifier, keyboardLayoutDescriptor);
+    }
+
+    public String[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
+            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+            @NonNull InputMethodSubtype imeSubtype) {
+        // TODO(b/259530132): Implement the new keyboard layout API: Returning list of all
+        //  layouts for now.
+        KeyboardLayout[] allLayouts = getKeyboardLayouts();
+        String[] allLayoutDesc = new String[allLayouts.length];
+        for (int i = 0; i < allLayouts.length; i++) {
+            allLayoutDesc[i] = allLayouts[i].getDescriptor();
+        }
+        return allLayoutDesc;
+    }
+
     public void switchKeyboardLayout(int deviceId, int direction) {
         mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
     }
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 39b9f1f..783a6ae 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -38,6 +38,7 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -59,6 +60,10 @@
  */
 public class LocaleManagerService extends SystemService {
     private static final String TAG = "LocaleManagerService";
+    // The feature flag control that allows the active IME to query the locales of the foreground
+    // app.
+    private static final String PROP_ALLOW_IME_QUERY_APP_LOCALE =
+            "i18n.feature.allow_ime_query_app_locale";
     final Context mContext;
     private final LocaleManagerService.LocaleManagerBinderService mBinderService;
     private ActivityTaskManagerInternal mActivityTaskManagerInternal;
@@ -431,6 +436,10 @@
      * Checks if the calling app is the current input method.
      */
     private boolean isCallerFromCurrentInputMethod(int userId) {
+        if (!SystemProperties.getBoolean(PROP_ALLOW_IME_QUERY_APP_LOCALE, true)) {
+            return false;
+        }
+
         String currentInputMethod = Settings.Secure.getStringForUser(
                 mContext.getContentResolver(),
                 Settings.Secure.DEFAULT_INPUT_METHOD,
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 25e71e8..99d77d6 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -69,7 +69,7 @@
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.database.sqlite.SQLiteDatabase;
-import android.hardware.authsecret.V1_0.IAuthSecret;
+import android.hardware.authsecret.IAuthSecret;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.face.Face;
 import android.hardware.face.FaceManager;
@@ -265,7 +265,8 @@
     protected boolean mHasSecureLockScreen;
 
     protected IGateKeeperService mGateKeeperService;
-    protected IAuthSecret mAuthSecretService;
+    protected IAuthSecret mAuthSecretServiceAidl;
+    protected android.hardware.authsecret.V1_0.IAuthSecret mAuthSecretServiceHidl;
 
     private static final String GSI_RUNNING_PROP = "ro.gsid.image_running";
 
@@ -550,16 +551,6 @@
             return (BiometricManager) mContext.getSystemService(Context.BIOMETRIC_SERVICE);
         }
 
-        public int settingsGlobalGetInt(ContentResolver contentResolver, String keyName,
-                int defaultValue) {
-            return Settings.Global.getInt(contentResolver, keyName, defaultValue);
-        }
-
-        public int settingsSecureGetInt(ContentResolver contentResolver, String keyName,
-                int defaultValue, int userId) {
-            return Settings.Secure.getIntForUser(contentResolver, keyName, defaultValue, userId);
-        }
-
         public java.security.KeyStore getJavaKeyStore() {
             try {
                 java.security.KeyStore ks = java.security.KeyStore.getInstance(
@@ -843,12 +834,19 @@
     }
 
     private void getAuthSecretHal() {
-        try {
-            mAuthSecretService = IAuthSecret.getService(/* retry */ true);
-        } catch (NoSuchElementException e) {
-            Slog.i(TAG, "Device doesn't implement AuthSecret HAL");
-        } catch (RemoteException e) {
-            Slog.w(TAG, "Failed to get AuthSecret HAL", e);
+        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");
+
+            try {
+                mAuthSecretServiceHidl =
+                    android.hardware.authsecret.V1_0.IAuthSecret.getService(/* retry */ true);
+            } catch (NoSuchElementException e) {
+                Slog.i(TAG, "Device doesn't implement AuthSecret HAL(hidl)");
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Failed to get AuthSecret HAL(hidl)", e);
+            }
         }
     }
 
@@ -1027,9 +1025,9 @@
 
     private void enforceFrpResolved() {
         final ContentResolver cr = mContext.getContentResolver();
-        final boolean inSetupWizard = mInjector.settingsSecureGetInt(cr,
+        final boolean inSetupWizard = Settings.Secure.getIntForUser(cr,
                 Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_SYSTEM) == 0;
-        final boolean secureFrp = mInjector.settingsSecureGetInt(cr,
+        final boolean secureFrp = Settings.Secure.getIntForUser(cr,
                 Settings.Secure.SECURE_FRP_MODE, 0, UserHandle.USER_SYSTEM) == 1;
         if (inSetupWizard && secureFrp) {
             throw new SecurityException("Cannot change credential in SUW while factory reset"
@@ -2155,7 +2153,7 @@
         if (credential == null || credential.isNone()) {
             throw new IllegalArgumentException("Credential can't be null or empty");
         }
-        if (userId == USER_FRP && mInjector.settingsGlobalGetInt(mContext.getContentResolver(),
+        if (userId == USER_FRP && Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
             Slog.e(TAG, "FRP credential can only be verified prior to provisioning.");
             return VerifyCredentialResponse.ERROR;
@@ -2611,17 +2609,25 @@
         // 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 && userId == UserHandle.USER_SYSTEM &&
+        if (userId == UserHandle.USER_SYSTEM &&
                 mUserManager.getUserInfo(userId).isPrimary()) {
-            try {
-                final byte[] rawSecret = sp.deriveVendorAuthSecret();
-                final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
-                for (int i = 0; i < rawSecret.length; ++i) {
-                    secret.add(rawSecret[i]);
+            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);
                 }
-                mAuthSecretService.primaryUserCredential(secret);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL", 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);
+                }
             }
         }
     }
@@ -3282,6 +3288,7 @@
             for (UserInfo user : users) {
                 if (userOwnsFrpCredential(mContext, user)) {
                     if (!isUserSecure(user.id)) {
+                        Slogf.d(TAG, "Clearing FRP credential tied to user %d", user.id);
                         mStorage.writePersistentDataBlock(PersistentData.TYPE_NONE, user.id,
                                 0, null);
                     }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 807ba3c..473c4b6 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -550,7 +550,7 @@
         mCache.clear();
     }
 
-    @Nullable @VisibleForTesting
+    @Nullable
     PersistentDataBlockManagerInternal getPersistentDataBlockManager() {
         if (mPersistentDataBlockManagerInternal == null) {
             mPersistentDataBlockManagerInternal =
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 73a16fd..acd7cc1 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -32,6 +32,7 @@
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.security.GateKeeper;
 import android.security.Scrypt;
 import android.service.gatekeeper.GateKeeperResponse;
@@ -457,6 +458,11 @@
         mPasswordSlotManager = passwordSlotManager;
     }
 
+    private boolean isDeviceProvisioned() {
+        return Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.DEVICE_PROVISIONED, 0) != 0;
+    }
+
     @VisibleForTesting
     protected IWeaver getWeaverService() throws RemoteException {
         try {
@@ -770,6 +776,17 @@
     private int getNextAvailableWeaverSlot() {
         Set<Integer> usedSlots = getUsedWeaverSlots();
         usedSlots.addAll(mPasswordSlotManager.getUsedSlots());
+        // If the device is not yet provisioned, then the Weaver slot used by the FRP credential may
+        // be still needed and must not be reused yet.  (This *should* instead check "has FRP been
+        // resolved yet?", which would allow reusing the slot a bit earlier.  However, the
+        // SECURE_FRP_MODE setting gets set to 1 too late for it to be used here.)
+        if (!isDeviceProvisioned()) {
+            PersistentData persistentData = mStorage.readPersistentDataBlock();
+            if (persistentData != null && persistentData.type == PersistentData.TYPE_SP_WEAVER) {
+                int slot = persistentData.userId; // Note: field name is misleading
+                usedSlots.add(slot);
+            }
+        }
         for (int i = 0; i < mWeaverConfig.slots; i++) {
             if (!usedSlots.contains(i)) {
                 return i;
@@ -814,9 +831,14 @@
 
             protectorSecret = transformUnderWeaverSecret(stretchedLskf, weaverSecret);
         } else {
-            // Weaver is unavailable, so make the protector use Gatekeeper to verify the LSKF
-            // instead.  However, skip Gatekeeper when the LSKF is empty, since it wouldn't give any
-            // benefit in that case as Gatekeeper isn't expected to provide secure deletion.
+            // Weaver is unavailable, so make the protector use Gatekeeper (GK) to verify the LSKF.
+            //
+            // However, skip GK when the LSKF is empty.  There are two reasons for this, one
+            // performance and one correctness.  The performance reason is that GK wouldn't give any
+            // benefit with an empty LSKF anyway, since GK isn't expected to provide secure
+            // deletion.  The correctness reason is that it is unsafe to enroll a password in the
+            // 'fakeUserId' GK range on an FRP-protected device that is in the setup wizard with FRP
+            // not passed yet, as that may overwrite the enrollment used by the FRP credential.
             if (!credential.isNone()) {
                 // In case GK enrollment leaves persistent state around (in RPMB), this will nuke
                 // them to prevent them from accumulating and causing problems.
@@ -908,12 +930,40 @@
         }
     }
 
+    private static boolean isNoneCredential(PasswordData pwd) {
+        return pwd == null || pwd.credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE;
+    }
+
+    private boolean shouldSynchronizeFrpCredential(@Nullable PasswordData pwd, int userId) {
+        if (mStorage.getPersistentDataBlockManager() == null) {
+            return false;
+        }
+        UserInfo userInfo = mUserManager.getUserInfo(userId);
+        if (!LockPatternUtils.userOwnsFrpCredential(mContext, userInfo)) {
+            return false;
+        }
+        // When initializing the synthetic password of the user that will own the FRP credential,
+        // the FRP data block must not be cleared if the device isn't provisioned yet, since in this
+        // case the old value of the block may still be needed for the FRP authentication step.  The
+        // FRP data block will instead be cleared later, by
+        // LockSettingsService.DeviceProvisionedObserver.clearFrpCredentialIfOwnerNotSecure().
+        //
+        // Don't check the SECURE_FRP_MODE setting here, as it gets set to 1 too late.
+        //
+        // Don't delay anything for a nonempty credential.  A nonempty credential can be set before
+        // the device has been provisioned, but it's guaranteed to be after FRP was resolved.
+        if (isNoneCredential(pwd) && !isDeviceProvisioned()) {
+            Slog.d(TAG, "Not clearing FRP credential yet because device is not yet provisioned");
+            return false;
+        }
+        return true;
+    }
+
     private void synchronizeFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
             int userId) {
-        if (mStorage.getPersistentDataBlockManager() != null
-                && LockPatternUtils.userOwnsFrpCredential(mContext,
-                mUserManager.getUserInfo(userId))) {
-            if (pwd != null && pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+        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());
             } else {
@@ -924,10 +974,9 @@
 
     private void synchronizeWeaverFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
             int userId, int weaverSlot) {
-        if (mStorage.getPersistentDataBlockManager() != null
-                && LockPatternUtils.userOwnsFrpCredential(mContext,
-                mUserManager.getUserInfo(userId))) {
-            if (pwd != null && pwd.credentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+        if (shouldSynchronizeFrpCredential(pwd, userId)) {
+            Slogf.d(TAG, "Syncing Weaver-based FRP credential tied to user %d", userId);
+            if (!isNoneCredential(pwd)) {
                 mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, weaverSlot,
                         requestedQuality, pwd.toBytes());
             } else {
@@ -1058,7 +1107,7 @@
         AuthenticationResult result = new AuthenticationResult();
 
         if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
-            // This should never happen, due to the migration done in LSS.bootCompleted().
+            // This should never happen, due to the migration done in LSS.onThirdPartyAppsStarted().
             Slogf.wtf(TAG, "Synthetic password not found for user %d", userId);
             result.gkResponse = VerifyCredentialResponse.ERROR;
             return result;
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index 4bbd40d..5f8572b 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -59,6 +59,9 @@
 
     static final int CONCURRENT_SNOOZE_LIMIT = 500;
 
+    // A safe size for strings to be put in persistent storage, to avoid breaking the XML write.
+    static final int MAX_STRING_LENGTH = 1000;
+
     protected static final String XML_TAG_NAME = "snoozed-notifications";
 
     private static final String XML_SNOOZED_NOTIFICATION = "notification";
@@ -200,7 +203,7 @@
         scheduleRepost(key, duration);
         Long activateAt = System.currentTimeMillis() + duration;
         synchronized (mLock) {
-            mPersistedSnoozedNotifications.put(key, activateAt);
+            mPersistedSnoozedNotifications.put(getTrimmedString(key), activateAt);
         }
     }
 
@@ -210,7 +213,10 @@
     protected void snooze(NotificationRecord record, String contextId) {
         if (contextId != null) {
             synchronized (mLock) {
-                mPersistedSnoozedNotificationsWithContext.put(record.getKey(), contextId);
+                mPersistedSnoozedNotificationsWithContext.put(
+                        getTrimmedString(record.getKey()),
+                        getTrimmedString(contextId)
+                );
             }
         }
         snooze(record);
@@ -225,6 +231,13 @@
         }
     }
 
+    private String getTrimmedString(String key) {
+        if (key != null && key.length() > MAX_STRING_LENGTH) {
+            return key.substring(0, MAX_STRING_LENGTH);
+        }
+        return key;
+    }
+
     protected boolean cancel(int userId, String pkg, String tag, int id) {
         synchronized (mLock) {
             final Set<Map.Entry<String, NotificationRecord>> records =
@@ -293,10 +306,12 @@
     }
 
     protected void repost(String key, int userId, boolean muteOnReturn) {
+        final String trimmedKey = getTrimmedString(key);
+
         NotificationRecord record;
         synchronized (mLock) {
-            mPersistedSnoozedNotifications.remove(key);
-            mPersistedSnoozedNotificationsWithContext.remove(key);
+            mPersistedSnoozedNotifications.remove(trimmedKey);
+            mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
             record = mSnoozedNotifications.remove(key);
         }
 
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 3421eb7..79f2b3f 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -379,6 +379,8 @@
             final String packageName = data.getSchemeSpecificPart();
 
             final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
+            final boolean systemUpdateUninstall =
+                    intent.getBooleanExtra(Intent.EXTRA_SYSTEM_UPDATE_UNINSTALL, false);
 
             final int[] userIds;
             final int extraUid = intent.getIntExtra(Intent.EXTRA_UID, UserHandle.USER_NULL);
@@ -405,7 +407,7 @@
                     break;
                 case ACTION_PACKAGE_REMOVED:
                     if (replacing) {
-                        onPackageReplacing(packageName, userIds);
+                        onPackageReplacing(packageName, systemUpdateUninstall, userIds);
                     } else {
                         onPackageRemoved(packageName, userIds);
                     }
@@ -463,7 +465,7 @@
         }
 
         private void onPackageReplacing(@NonNull final String packageName,
-                @NonNull final int[] userIds) {
+                boolean systemUpdateUninstall, @NonNull final int[] userIds) {
             try {
                 traceBegin(TRACE_TAG_RRO, "OMS#onPackageReplacing " + packageName);
                 for (int userId : userIds) {
@@ -472,8 +474,8 @@
                                 packageName, userId);
                         if (pkg != null && !mPackageManager.isInstantApp(packageName, userId)) {
                             try {
-                                updateTargetPackagesLocked(
-                                        mImpl.onPackageReplacing(packageName, userId));
+                                updateTargetPackagesLocked(mImpl.onPackageReplacing(packageName,
+                                        systemUpdateUninstall, userId));
                             } catch (OperationFailedException e) {
                                 Slog.e(TAG, "onPackageReplacing internal error", e);
                             }
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 6ffe60d..9d5830c 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -21,6 +21,7 @@
 import static android.content.om.OverlayInfo.STATE_MISSING_TARGET;
 import static android.content.om.OverlayInfo.STATE_NO_IDMAP;
 import static android.content.om.OverlayInfo.STATE_OVERLAY_IS_BEING_REPLACED;
+import static android.content.om.OverlayInfo.STATE_SYSTEM_UPDATE_UNINSTALL;
 import static android.content.om.OverlayInfo.STATE_TARGET_IS_BEING_REPLACED;
 import static android.os.UserHandle.USER_SYSTEM;
 
@@ -78,6 +79,7 @@
 
     // Flags to use in conjunction with updateState.
     private static final int FLAG_OVERLAY_IS_BEING_REPLACED = 1 << 1;
+    private static final int FLAG_SYSTEM_UPDATE_UNINSTALL = 1 << 2;
 
     private final PackageManagerHelper mPackageManager;
     private final IdmapManager mIdmapManager;
@@ -275,9 +277,13 @@
     }
 
     @NonNull
-    Set<UserPackage> onPackageReplacing(@NonNull final String pkgName, final int userId)
-            throws OperationFailedException {
-        return reconcileSettingsForPackage(pkgName, userId, FLAG_OVERLAY_IS_BEING_REPLACED);
+    Set<UserPackage> onPackageReplacing(@NonNull final String pkgName,
+            boolean systemUpdateUninstall, final int userId) throws OperationFailedException {
+        int flags = FLAG_OVERLAY_IS_BEING_REPLACED;
+        if (systemUpdateUninstall) {
+            flags |= FLAG_SYSTEM_UPDATE_UNINSTALL;
+        }
+        return reconcileSettingsForPackage(pkgName, userId, flags);
     }
 
     @NonNull
@@ -840,6 +846,10 @@
             return STATE_OVERLAY_IS_BEING_REPLACED;
         }
 
+        if ((flags & FLAG_SYSTEM_UPDATE_UNINSTALL) != 0) {
+            return STATE_SYSTEM_UPDATE_UNINSTALL;
+        }
+
         if (targetPackage == null) {
             return STATE_MISSING_TARGET;
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerLocal.java b/services/core/java/com/android/server/pm/PackageManagerLocal.java
index 21c2f2c..fad61b8 100644
--- a/services/core/java/com/android/server/pm/PackageManagerLocal.java
+++ b/services/core/java/com/android/server/pm/PackageManagerLocal.java
@@ -167,14 +167,16 @@
         PackageState getPackageState(@NonNull String packageName);
 
         /**
-         * Iterates on all states. This should only be used when either the target package name
-         * is not known or the large majority of the states are expected to be used.
-         *
+         * Returns a map of all {@link PackageState PackageStates} on the device.
+         * <p>
          * This will cause app visibility filtering to be invoked on each state on the device,
-         * which can be expensive.
+         * which can be expensive. Prefer {@link #getPackageState(String)} if possible.
          *
-         * @param consumer Block to accept each state as it becomes available post-filtering.
+         * @return Mapping of package name to {@link PackageState}.
          */
+        @NonNull
+        Map<String, PackageState> getPackageStates();
+
         void forAllPackageStates(@NonNull Consumer<PackageState> consumer);
 
         @Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9e1bffb..cf59a1e 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -7317,6 +7317,7 @@
             }
 
             consumer.accept(mPackageStateMutator);
+            mPackageStateMutator.onFinished();
             onChanged();
         }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 77334e5..a72ae56 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -142,6 +142,11 @@
     public static final Predicate<PackageStateInternal> REMOVE_IF_NULL_PKG =
             pkgSetting -> pkgSetting.getPkg() == null;
 
+    // This is a horrible hack to workaround b/240373119, specifically for fixing the T branch.
+    // A proper fix should be implemented in master instead.
+    public static final ThreadLocal<Boolean> DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS =
+            ThreadLocal.withInitial(() -> false);
+
     /**
      * Components of apps targeting Android T and above will stop receiving intents from
      * external callers that do not match its declared intent filters.
@@ -1093,6 +1098,8 @@
             PlatformCompat compat, ComponentResolverApi resolver,
             List<ResolveInfo> resolveInfos, boolean isReceiver,
             Intent intent, String resolvedType, int filterCallingUid) {
+        if (DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.get()) return;
+
         final Printer logPrinter = DEBUG_INTENT_MATCHING
                 ? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM)
                 : null;
diff --git a/services/core/java/com/android/server/pm/PackageRemovedInfo.java b/services/core/java/com/android/server/pm/PackageRemovedInfo.java
index 4cac115..dd580a5 100644
--- a/services/core/java/com/android/server/pm/PackageRemovedInfo.java
+++ b/services/core/java/com/android/server/pm/PackageRemovedInfo.java
@@ -115,6 +115,7 @@
         final int removedUid = mRemovedAppId >= 0  ? mRemovedAppId : mUid;
         extras.putInt(Intent.EXTRA_UID, removedUid);
         extras.putBoolean(Intent.EXTRA_DATA_REMOVED, mDataRemoved);
+        extras.putBoolean(Intent.EXTRA_SYSTEM_UPDATE_UNINSTALL, mIsRemovedPackageSystemUpdate);
         extras.putBoolean(Intent.EXTRA_DONT_KILL_APP, !killApp);
         extras.putBoolean(Intent.EXTRA_USER_INITIATED, !removedBySystem);
         final boolean isReplace = mIsUpdate || mIsRemovedPackageSystemUpdate;
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 3ec6e7d..b18179e 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -664,7 +664,9 @@
                 mUserStates.put(other.mUserStates.keyAt(i),
                         other.mUserStates.valueAt(i).snapshot());
             } else {
-                mUserStates.put(other.mUserStates.keyAt(i), other.mUserStates.valueAt(i));
+                var userState = other.mUserStates.valueAt(i);
+                userState.setWatchable(this);
+                mUserStates.put(other.mUserStates.keyAt(i), userState);
             }
         }
 
@@ -1378,7 +1380,11 @@
         return mSecondaryCpuAbi;
     }
 
-
+    @ApplicationInfo.HiddenApiEnforcementPolicy
+    @Override
+    public int getHiddenApiEnforcementPolicy() {
+        return AndroidPackageUtils.getHiddenApiEnforcementPolicy(getAndroidPackage(), this);
+    }
 
     // Code below generated by codegen v1.0.23.
     //
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 4aba016..bc9f7b2 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -367,8 +367,12 @@
     @Watched(manual = true)
     private final RuntimePermissionPersistence mRuntimePermissionsPersistence;
 
+    // Current settings file.
     private final File mSettingsFilename;
-    private final File mBackupSettingsFilename;
+    // Previous settings file.
+    // Removed when the current settings file successfully stored.
+    private final File mPreviousSettingsFilename;
+
     private final File mPackageListFilename;
     private final File mStoppedPackagesFilename;
     private final File mBackupStoppedPackagesFilename;
@@ -635,7 +639,7 @@
         mRuntimePermissionsPersistence = null;
         mPermissionDataProvider = null;
         mSettingsFilename = null;
-        mBackupSettingsFilename = null;
+        mPreviousSettingsFilename = null;
         mPackageListFilename = null;
         mStoppedPackagesFilename = null;
         mBackupStoppedPackagesFilename = null;
@@ -706,7 +710,7 @@
                 |FileUtils.S_IROTH|FileUtils.S_IXOTH,
                 -1, -1);
         mSettingsFilename = new File(mSystemDir, "packages.xml");
-        mBackupSettingsFilename = new File(mSystemDir, "packages-backup.xml");
+        mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
         mPackageListFilename = new File(mSystemDir, "packages.list");
         FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);
 
@@ -747,7 +751,7 @@
         mLock = null;
         mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence;
         mSettingsFilename = null;
-        mBackupSettingsFilename = null;
+        mPreviousSettingsFilename = null;
         mPackageListFilename = null;
         mStoppedPackagesFilename = null;
         mBackupStoppedPackagesFilename = null;
@@ -2572,10 +2576,10 @@
             // to persist settings earlier. So preserve the older
             // backup for future reference since the current settings
             // might have been corrupted.
-            if (!mBackupSettingsFilename.exists()) {
-                if (!mSettingsFilename.renameTo(mBackupSettingsFilename)) {
+            if (!mPreviousSettingsFilename.exists()) {
+                if (!mSettingsFilename.renameTo(mPreviousSettingsFilename)) {
                     Slog.wtf(PackageManagerService.TAG,
-                            "Unable to backup package manager settings, "
+                            "Unable to store older package manager settings, "
                             + " current changes will be lost at reboot");
                     return;
                 }
@@ -2669,9 +2673,9 @@
             FileUtils.sync(fstr);
             fstr.close();
 
-            // New settings successfully written, old ones are no longer
-            // needed.
-            mBackupSettingsFilename.delete();
+            // New settings successfully written, old ones are no longer needed.
+            mPreviousSettingsFilename.delete();
+
             FileUtils.setPermissions(mSettingsFilename.toString(),
                     FileUtils.S_IRUSR|FileUtils.S_IWUSR
                     |FileUtils.S_IRGRP|FileUtils.S_IWGRP,
@@ -3109,16 +3113,15 @@
 
     boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) {
         FileInputStream str = null;
-        if (mBackupSettingsFilename.exists()) {
+        if (mPreviousSettingsFilename.exists()) {
             try {
-                str = new FileInputStream(mBackupSettingsFilename);
+                str = new FileInputStream(mPreviousSettingsFilename);
                 mReadMessages.append("Reading from backup settings file\n");
                 PackageManagerService.reportSettingsProblem(Log.INFO,
                         "Need to read from backup settings file");
                 if (mSettingsFilename.exists()) {
-                    // If both the backup and settings file exist, we
-                    // ignore the settings since it might have been
-                    // corrupted.
+                    // If both the previous and current settings files exist,
+                    // we ignore the current since it might have been corrupted.
                     Slog.w(PackageManagerService.TAG, "Cleaning up settings file "
                             + mSettingsFilename);
                     mSettingsFilename.delete();
@@ -5594,8 +5597,8 @@
     }
 
     private static final class RuntimePermissionPersistence {
-        // 200-400ms delay to avoid monopolizing PMS lock when written for multiple users.
-        private static final long WRITE_PERMISSIONS_DELAY_MILLIS = 300;
+        // 700-1300ms delay to avoid monopolizing PMS lock when written for multiple users.
+        private static final long WRITE_PERMISSIONS_DELAY_MILLIS = 1000;
         private static final double WRITE_PERMISSIONS_DELAY_JITTER = 0.3;
 
         private static final long MAX_WRITE_PERMISSIONS_DELAY_MILLIS = 2000;
@@ -5613,8 +5616,7 @@
 
         // Low-priority handlers running on SystemBg thread.
         private final Handler mAsyncHandler = new MyHandler();
-        private final Handler mPersistenceHandler = new Handler(
-                BackgroundThread.getHandler().getLooper());
+        private final Handler mPersistenceHandler = new PersistenceHandler();
 
         private final Object mLock = new Object();
 
@@ -5761,20 +5763,22 @@
                 @NonNull WatchedArrayMap<String, SharedUserSetting> sharedUsers,
                 @Nullable Handler pmHandler, @NonNull Object pmLock,
                 boolean sync) {
-            final int version;
-            final String fingerprint;
-            final boolean isLegacyPermissionStateStale;
             synchronized (mLock) {
                 mAsyncHandler.removeMessages(userId);
                 mWriteScheduled.delete(userId);
-
-                version = mVersions.get(userId, INITIAL_VERSION);
-                fingerprint = mFingerprints.get(userId);
-                isLegacyPermissionStateStale = mIsLegacyPermissionStateStale;
-                mIsLegacyPermissionStateStale = false;
             }
 
             Runnable writer = () -> {
+                final int version;
+                final String fingerprint;
+                final boolean isLegacyPermissionStateStale;
+                synchronized (mLock) {
+                    version = mVersions.get(userId, INITIAL_VERSION);
+                    fingerprint = mFingerprints.get(userId);
+                    isLegacyPermissionStateStale = mIsLegacyPermissionStateStale;
+                    mIsLegacyPermissionStateStale = false;
+                }
+
                 final RuntimePermissionsState runtimePermissions;
                 synchronized (pmLock) {
                     if (sync || isLegacyPermissionStateStale) {
@@ -5823,7 +5827,7 @@
                 }
                 if (pmHandler != null) {
                     // Async version.
-                    mPersistenceHandler.post(() -> writePendingStates());
+                    mPersistenceHandler.obtainMessage(userId).sendToTarget();
                 } else {
                     // Sync version.
                     writePendingStates();
@@ -6099,6 +6103,17 @@
                 }
             }
         }
+
+        private final class PersistenceHandler extends Handler {
+            PersistenceHandler() {
+                super(BackgroundThread.getHandler().getLooper());
+            }
+
+            @Override
+            public void handleMessage(Message message) {
+                writePendingStates();
+            }
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
index f388e07..9f21097 100644
--- a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
+++ b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
@@ -298,7 +298,8 @@
                     dexMetadataType,
                     apkType,
                     ISA_MAP.getOrDefault(isa,
-                            ArtStatsLog.ART_DATUM_REPORTED__ISA__ART_ISA_UNKNOWN));
+                            ArtStatsLog.ART_DATUM_REPORTED__ISA__ART_ISA_UNKNOWN),
+                    ArtStatsLog.ART_DATUM_REPORTED__GC__ART_GC_COLLECTOR_TYPE_UNKNOWN);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
index 4ff0d59..024b63e 100644
--- a/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
+++ b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
@@ -22,6 +22,7 @@
 import android.annotation.UserIdInt;
 import android.os.Binder;
 import android.os.UserHandle;
+import android.util.ArrayMap;
 
 import com.android.server.pm.Computer;
 import com.android.server.pm.PackageManagerLocal;
@@ -30,7 +31,6 @@
 import com.android.server.pm.snapshot.PackageDataSnapshot;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -143,7 +143,7 @@
         private final int mUserId;
 
         @Nullable
-        private ArrayList<PackageState> mFilteredPackageStates;
+        private Map<String, PackageState> mFilteredPackageStates;
 
         @Nullable
         private final UnfilteredSnapshotImpl mParentSnapshot;
@@ -179,25 +179,36 @@
             return mSnapshot.getPackageStateFiltered(packageName, mCallingUid, mUserId);
         }
 
+        @NonNull
         @Override
-        public void forAllPackageStates(@NonNull Consumer<PackageState> consumer) {
+        public Map<String, PackageState> getPackageStates() {
             checkClosed();
 
             if (mFilteredPackageStates == null) {
                 var packageStates = mSnapshot.getPackageStates();
-                var filteredPackageStates = new ArrayList<PackageState>();
+                var filteredPackageStates = new ArrayMap<String, PackageState>();
                 for (int index = 0, size = packageStates.size(); index < size; index++) {
                     var packageState = packageStates.valueAt(index);
                     if (!mSnapshot.shouldFilterApplication(packageState, mCallingUid, mUserId)) {
-                        filteredPackageStates.add(packageState);
+                        filteredPackageStates.put(packageStates.keyAt(index), packageState);
                     }
                 }
-                mFilteredPackageStates = filteredPackageStates;
+                mFilteredPackageStates = Collections.unmodifiableMap(filteredPackageStates);
             }
 
-            for (int index = 0, size = mFilteredPackageStates.size(); index < size; index++) {
-                var packageState = mFilteredPackageStates.get(index);
-                consumer.accept(packageState);
+            return mFilteredPackageStates;
+        }
+
+        @Override
+        public void forAllPackageStates(@NonNull Consumer<PackageState> consumer) {
+            checkClosed();
+
+            var packageStates = mSnapshot.getPackageStates();
+            for (int index = 0, size = packageStates.size(); index < size; index++) {
+                var packageState = packageStates.valueAt(index);
+                if (!mSnapshot.shouldFilterApplication(packageState, mCallingUid, mUserId)) {
+                    consumer.accept(packageState);
+                }
             }
         }
     }
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
index c76b129..82b5fa2 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
@@ -234,10 +234,12 @@
                 || !pkg.getLibraryNames().isEmpty();
     }
 
-    public static int getHiddenApiEnforcementPolicy(AndroidPackage pkg,
+    public static int getHiddenApiEnforcementPolicy(@Nullable AndroidPackage pkg,
             @NonNull PackageStateInternal pkgSetting) {
         boolean isAllowedToUseHiddenApis;
-        if (pkg.isSignedWithPlatformKey()) {
+        if (pkg == null) {
+            isAllowedToUseHiddenApis = false;
+        } else if (pkg.isSignedWithPlatformKey()) {
             isAllowedToUseHiddenApis = true;
         } else if (pkg.isSystem() || pkgSetting.getTransientState().isUpdatedSystemApp()) {
             isAllowedToUseHiddenApis = pkg.isUsesNonSdkApi()
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 9ec63fc..cefe9cd 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -467,7 +467,7 @@
 
     @Override
     public PermissionInfo getPermissionInfo(String permissionName, String packageName, int flags) {
-        return mPermissionManagerServiceImpl.getPermissionInfo(permissionName, packageName, flags);
+        return mPermissionManagerServiceImpl.getPermissionInfo(permissionName, flags, packageName);
     }
 
     @Override
@@ -792,14 +792,14 @@
 
         @NonNull
         @Override
-        public ArrayList<PermissionInfo> getAllPermissionsWithProtection(
+        public List<PermissionInfo> getAllPermissionsWithProtection(
                 @PermissionInfo.Protection int protection) {
             return mPermissionManagerServiceImpl.getAllPermissionsWithProtection(protection);
         }
 
         @NonNull
         @Override
-        public ArrayList<PermissionInfo> getAllPermissionsWithProtectionFlags(
+        public List<PermissionInfo> getAllPermissionsWithProtectionFlags(
                 @PermissionInfo.ProtectionFlags int protectionFlags) {
             return mPermissionManagerServiceImpl
                     .getAllPermissionsWithProtectionFlags(protectionFlags);
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 5ffbbdc..e56edeb 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -559,8 +559,8 @@
 
     @Override
     @Nullable
-    public PermissionInfo getPermissionInfo(@NonNull String permName, @NonNull String opPackageName,
-            @PackageManager.PermissionInfoFlags int flags) {
+    public PermissionInfo getPermissionInfo(@NonNull String permName,
+            @PackageManager.PermissionInfoFlags int flags, @NonNull String opPackageName) {
         final int callingUid = Binder.getCallingUid();
         if (mPackageManagerInt.getInstantAppPackageName(callingUid) != null) {
             return null;
@@ -2127,7 +2127,7 @@
             for (int i = 0; i < numRequestedPermissions; i++) {
                 PermissionInfo permInfo = getPermissionInfo(
                         newPackage.getRequestedPermissions().get(i),
-                        newPackage.getPackageName(), 0);
+                        0, newPackage.getPackageName());
                 if (permInfo == null) {
                     continue;
                 }
@@ -5204,9 +5204,9 @@
 
     @NonNull
     @Override
-    public ArrayList<PermissionInfo> getAllPermissionsWithProtection(
+    public List<PermissionInfo> getAllPermissionsWithProtection(
             @PermissionInfo.Protection int protection) {
-        ArrayList<PermissionInfo> matchingPermissions = new ArrayList<>();
+        List<PermissionInfo> matchingPermissions = new ArrayList<>();
 
         synchronized (mLock) {
             for (final Permission permission : mRegistry.getPermissions()) {
@@ -5221,9 +5221,9 @@
 
     @NonNull
     @Override
-    public ArrayList<PermissionInfo> getAllPermissionsWithProtectionFlags(
+    public List<PermissionInfo> getAllPermissionsWithProtectionFlags(
             @PermissionInfo.ProtectionFlags int protectionFlags) {
-        ArrayList<PermissionInfo> matchingPermissions = new ArrayList<>();
+        List<PermissionInfo> matchingPermissions = new ArrayList<>();
 
         synchronized (mLock) {
             for (final Permission permission : mRegistry.getPermissions()) {
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
index 930936b..d9caec7 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
@@ -32,7 +32,6 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -77,8 +76,8 @@
      * @return a {@link PermissionInfo} containing information about the permission, or {@code null}
      *         if not found
      */
-    PermissionInfo getPermissionInfo(@NonNull String permName, @NonNull String opPackageName,
-            @PackageManager.PermissionInfoFlags int flags);
+    PermissionInfo getPermissionInfo(@NonNull String permName,
+            @PackageManager.PermissionInfoFlags int flags, @NonNull String opPackageName);
 
     /**
      * Query for all of the permissions associated with a particular group.
@@ -487,11 +486,11 @@
 
     /** Get all permissions that have a certain protection */
     @NonNull
-    ArrayList<PermissionInfo> getAllPermissionsWithProtection(
+    List<PermissionInfo> getAllPermissionsWithProtection(
             @PermissionInfo.Protection int protection);
 
     /** Get all permissions that have certain protection flags */
-    @NonNull ArrayList<PermissionInfo> getAllPermissionsWithProtectionFlags(
+    @NonNull List<PermissionInfo> getAllPermissionsWithProtectionFlags(
             @PermissionInfo.ProtectionFlags int protectionFlags);
 
     /**
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
index f20620e..97ac749 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
@@ -164,11 +164,12 @@
 
     /** Get all permissions that have a certain protection */
     @NonNull
-    ArrayList<PermissionInfo> getAllPermissionsWithProtection(
+    List<PermissionInfo> getAllPermissionsWithProtection(
             @PermissionInfo.Protection int protection);
 
-    /** Get all permissions that have certain protection flags */
-    @NonNull ArrayList<PermissionInfo> getAllPermissionsWithProtectionFlags(
+    /** Get all permissions that have certain protection flags
+     * @return*/
+    @NonNull List<PermissionInfo> getAllPermissionsWithProtectionFlags(
             @PermissionInfo.ProtectionFlags int protectionFlags);
 
     /**
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index e8d0640..67b7647 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -112,6 +112,19 @@
     int getAppId();
 
     /**
+     * Retrieves effective hidden API policy for this app. The state can be dependent on
+     * {@link #getAndroidPackage()} availability and whether the app is a system app.
+     *
+     * Note that during process start, this policy may be mutated by device specific process
+     * configuration, so this value isn't truly final.
+     *
+     * @return The (mostly) final {@link ApplicationInfo.HiddenApiEnforcementPolicy} that should be
+     * applied to this package.
+     */
+    @ApplicationInfo.HiddenApiEnforcementPolicy
+    int getHiddenApiEnforcementPolicy();
+
+    /**
      * @see PackageInfo#packageName
      * @see AndroidPackage#getPackageName()
      */
@@ -139,6 +152,18 @@
     String getSeInfo();
 
     /**
+     * @return State for a user or {@link PackageUserState#DEFAULT} if the state doesn't exist.
+     */
+    @NonNull
+    PackageUserState getStateForUser(@NonNull UserHandle user);
+
+    /**
+     * @see R.styleable#AndroidManifestUsesLibrary
+     */
+    @NonNull
+    List<SharedLibrary> getUsesLibraries();
+
+    /**
      * @see AndroidPackage#isPrivileged()
      */
     boolean isPrivileged();
@@ -154,18 +179,6 @@
      */
     boolean isUpdatedSystemApp();
 
-    /**
-     * @return State for a user or {@link PackageUserState#DEFAULT} if the state doesn't exist.
-     */
-    @NonNull
-    PackageUserState getStateForUser(@NonNull UserHandle user);
-
-    /**
-     * @see R.styleable#AndroidManifestUsesLibrary
-     */
-    @NonNull
-    List<SharedLibrary> getUsesLibraries();
-
     // Methods below this comment are not yet exposed as API
 
     /**
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
index e552a34..43d019a 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
@@ -19,6 +19,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.SigningInfo;
@@ -118,6 +119,8 @@
     private final int mCategoryOverride;
     @Nullable
     private final String mCpuAbiOverride;
+    @ApplicationInfo.HiddenApiEnforcementPolicy
+    private final int mHiddenApiEnforcementPolicy;
     private final long mLastModifiedTime;
     private final long mLastUpdateTime;
     private final long mLongVersionCode;
@@ -170,6 +173,7 @@
         mAppId = pkgState.getAppId();
         mCategoryOverride = pkgState.getCategoryOverride();
         mCpuAbiOverride = pkgState.getCpuAbiOverride();
+        mHiddenApiEnforcementPolicy = pkgState.getHiddenApiEnforcementPolicy();
         mLastModifiedTime = pkgState.getLastModifiedTime();
         mLastUpdateTime = pkgState.getLastUpdateTime();
         mLongVersionCode = pkgState.getVersionCode();
@@ -545,7 +549,7 @@
         }
 
         @DataClass.Generated(
-                time = 1665778832625L,
+                time = 1666719622708L,
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
                 inputSignatures = "private  int mBooleans\nprivate final  long mCeDataInode\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mDisabledComponents\nprivate final @android.content.pm.PackageManager.DistractionRestriction int mDistractionFlags\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mEnabledComponents\nprivate final  int mEnabledState\nprivate final @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate final @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate final @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate final @android.annotation.NonNull android.content.pm.overlay.OverlayPaths mOverlayPaths\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate final @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate final @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate final  long mFirstInstallTime\npublic static  com.android.server.pm.pkg.PackageUserState copy(com.android.server.pm.pkg.PackageUserState)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isSuspended()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\nclass UserStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageUserState]\nprivate static final  int HIDDEN\nprivate static final  int INSTALLED\nprivate static final  int INSTANT_APP\nprivate static final  int NOT_LAUNCHED\nprivate static final  int STOPPED\nprivate static final  int SUSPENDED\nprivate static final  int VIRTUAL_PRELOAD\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
@@ -609,6 +613,11 @@
     }
 
     @DataClass.Generated.Member
+    public @ApplicationInfo.HiddenApiEnforcementPolicy int getHiddenApiEnforcementPolicy() {
+        return mHiddenApiEnforcementPolicy;
+    }
+
+    @DataClass.Generated.Member
     public long getLastModifiedTime() {
         return mLastModifiedTime;
     }
@@ -705,10 +714,10 @@
     }
 
     @DataClass.Generated(
-            time = 1665778832668L,
+            time = 1666719622749L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
-            inputSignatures = "private  int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final  int mAppId\nprivate final  int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final  long mLastModifiedTime\nprivate final  long mLastUpdateTime\nprivate final  long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSeInfo\nprivate final  boolean mHasSharedUser\nprivate final  int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\npublic static  com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override int getSharedUserAppId()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final  int SYSTEM\nprivate static final  int EXTERNAL_STORAGE\nprivate static final  int PRIVILEGED\nprivate static final  int OEM\nprivate static final  int VENDOR\nprivate static final  int PRODUCT\nprivate static final  int SYSTEM_EXT\nprivate static final  int REQUIRED_FOR_SYSTEM_USER\nprivate static final  int ODM\nprivate static final  int FORCE_QUERYABLE_OVERRIDE\nprivate static final  int HIDDEN_UNTIL_INSTALLED\nprivate static final  int INSTALL_PERMISSIONS_FIXED\nprivate static final  int UPDATE_AVAILABLE\nprivate static final  int UPDATED_SYSTEM_APP\nprivate static final  int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
+            inputSignatures = "private  int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final  int mAppId\nprivate final  int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy int mHiddenApiEnforcementPolicy\nprivate final  long mLastModifiedTime\nprivate final  long mLastUpdateTime\nprivate final  long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSeInfo\nprivate final  boolean mHasSharedUser\nprivate final  int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\npublic static  com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override int getSharedUserAppId()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final  int SYSTEM\nprivate static final  int EXTERNAL_STORAGE\nprivate static final  int PRIVILEGED\nprivate static final  int OEM\nprivate static final  int VENDOR\nprivate static final  int PRODUCT\nprivate static final  int SYSTEM_EXT\nprivate static final  int REQUIRED_FOR_SYSTEM_USER\nprivate static final  int ODM\nprivate static final  int FORCE_QUERYABLE_OVERRIDE\nprivate static final  int HIDDEN_UNTIL_INSTALLED\nprivate static final  int INSTALL_PERMISSIONS_FIXED\nprivate static final  int UPDATE_AVAILABLE\nprivate static final  int UPDATED_SYSTEM_APP\nprivate static final  int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
index a536f90..b3deb1c 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
@@ -44,7 +44,7 @@
 /** @hide */
 @DataClass(genConstructor = false, genBuilder = false, genEqualsHashCode = true)
 @DataClass.Suppress({"mOverlayPathsLock", "mOverlayPaths", "mSharedLibraryOverlayPathsLock",
-        "mSharedLibraryOverlayPaths", "setOverlayPaths", "setCachedOverlayPaths"})
+        "mSharedLibraryOverlayPaths", "setOverlayPaths", "setCachedOverlayPaths", "getWatchable"})
 public class PackageUserStateImpl extends WatchableImpl implements PackageUserStateInternal,
         Snappable {
 
@@ -92,8 +92,9 @@
 
     private long mFirstInstallTime;
 
+    // TODO(b/239050028): Remove, enforce notifying parent through PMS commit method
     @Nullable
-    private final Watchable mWatchable;
+    private Watchable mWatchable;
 
     @NonNull
     final SnapshotCache<PackageUserStateImpl> mSnapshot;
@@ -550,71 +551,30 @@
                 ? Collections.emptyMap() : mSharedLibraryOverlayPaths;
     }
 
-    @Override
-    public boolean equals(@Nullable Object o) {
-        // You can override field equality logic by defining either of the methods like:
-        // boolean fieldNameEquals(PackageUserStateImpl other) { ... }
-        // boolean fieldNameEquals(FieldType otherValue) { ... }
-
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        @SuppressWarnings("unchecked")
-        PackageUserStateImpl that = (PackageUserStateImpl) o;
-        //noinspection PointlessBooleanExpression
-        return Objects.equals(mDisabledComponentsWatched, that.mDisabledComponentsWatched)
-                && Objects.equals(mEnabledComponentsWatched, that.mEnabledComponentsWatched)
-                && mCeDataInode == that.mCeDataInode
-                && mInstalled == that.mInstalled
-                && mStopped == that.mStopped
-                && mNotLaunched == that.mNotLaunched
-                && mHidden == that.mHidden
-                && mDistractionFlags == that.mDistractionFlags
-                && mInstantApp == that.mInstantApp
-                && mVirtualPreload == that.mVirtualPreload
-                && mEnabledState == that.mEnabledState
-                && mInstallReason == that.mInstallReason
-                && mUninstallReason == that.mUninstallReason
-                && Objects.equals(mHarmfulAppWarning, that.mHarmfulAppWarning)
-                && Objects.equals(mLastDisableAppCaller, that.mLastDisableAppCaller)
-                && Objects.equals(mOverlayPaths, that.mOverlayPaths)
-                && Objects.equals(mSharedLibraryOverlayPaths, that.mSharedLibraryOverlayPaths)
-                && Objects.equals(mSplashScreenTheme, that.mSplashScreenTheme)
-                && Objects.equals(mSuspendParams, that.mSuspendParams)
-                && Objects.equals(mComponentLabelIconOverrideMap,
-                        that.mComponentLabelIconOverrideMap)
-                && mFirstInstallTime == that.mFirstInstallTime
-                && Objects.equals(mWatchable, that.mWatchable);
+    @NonNull
+    public PackageUserStateImpl setWatchable(@NonNull Watchable watchable) {
+        mWatchable = watchable;
+        return this;
     }
 
-    @Override
-    public int hashCode() {
-        // You can override field hashCode logic by defining methods like:
-        // int fieldNameHashCode() { ... }
+    private boolean watchableEquals(Watchable other) {
+        // Ignore the Watchable for equality
+        return true;
+    }
 
-        int _hash = 1;
-        _hash = 31 * _hash + Objects.hashCode(mDisabledComponentsWatched);
-        _hash = 31 * _hash + Objects.hashCode(mEnabledComponentsWatched);
-        _hash = 31 * _hash + Long.hashCode(mCeDataInode);
-        _hash = 31 * _hash + Boolean.hashCode(mInstalled);
-        _hash = 31 * _hash + Boolean.hashCode(mStopped);
-        _hash = 31 * _hash + Boolean.hashCode(mNotLaunched);
-        _hash = 31 * _hash + Boolean.hashCode(mHidden);
-        _hash = 31 * _hash + mDistractionFlags;
-        _hash = 31 * _hash + Boolean.hashCode(mInstantApp);
-        _hash = 31 * _hash + Boolean.hashCode(mVirtualPreload);
-        _hash = 31 * _hash + mEnabledState;
-        _hash = 31 * _hash + mInstallReason;
-        _hash = 31 * _hash + mUninstallReason;
-        _hash = 31 * _hash + Objects.hashCode(mHarmfulAppWarning);
-        _hash = 31 * _hash + Objects.hashCode(mLastDisableAppCaller);
-        _hash = 31 * _hash + Objects.hashCode(mOverlayPaths);
-        _hash = 31 * _hash + Objects.hashCode(mSharedLibraryOverlayPaths);
-        _hash = 31 * _hash + Objects.hashCode(mSplashScreenTheme);
-        _hash = 31 * _hash + Objects.hashCode(mSuspendParams);
-        _hash = 31 * _hash + Objects.hashCode(mComponentLabelIconOverrideMap);
-        _hash = 31 * _hash + Long.hashCode(mFirstInstallTime);
-        _hash = 31 * _hash + Objects.hashCode(mWatchable);
-        return _hash;
+    private int watchableHashCode() {
+        // Ignore the Watchable for equality
+        return 0;
+    }
+
+    private boolean snapshotEquals(SnapshotCache<PackageUserStateImpl> other) {
+        // Ignore the SnapshotCache for equality
+        return true;
+    }
+
+    private int snapshotHashCode() {
+        // Ignore the SnapshotCache for equality
+        return 0;
     }
 
 
@@ -736,11 +696,6 @@
     }
 
     @DataClass.Generated.Member
-    public @Nullable Watchable getWatchable() {
-        return mWatchable;
-    }
-
-    @DataClass.Generated.Member
     public @NonNull SnapshotCache<PackageUserStateImpl> getSnapshot() {
         return mSnapshot;
     }
@@ -778,11 +733,82 @@
         return this;
     }
 
+    @Override
+    @DataClass.Generated.Member
+    public boolean equals(@Nullable Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(PackageUserStateImpl other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        PackageUserStateImpl that = (PackageUserStateImpl) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && Objects.equals(mDisabledComponentsWatched, that.mDisabledComponentsWatched)
+                && Objects.equals(mEnabledComponentsWatched, that.mEnabledComponentsWatched)
+                && mCeDataInode == that.mCeDataInode
+                && mInstalled == that.mInstalled
+                && mStopped == that.mStopped
+                && mNotLaunched == that.mNotLaunched
+                && mHidden == that.mHidden
+                && mDistractionFlags == that.mDistractionFlags
+                && mInstantApp == that.mInstantApp
+                && mVirtualPreload == that.mVirtualPreload
+                && mEnabledState == that.mEnabledState
+                && mInstallReason == that.mInstallReason
+                && mUninstallReason == that.mUninstallReason
+                && Objects.equals(mHarmfulAppWarning, that.mHarmfulAppWarning)
+                && Objects.equals(mLastDisableAppCaller, that.mLastDisableAppCaller)
+                && Objects.equals(mOverlayPaths, that.mOverlayPaths)
+                && Objects.equals(mSharedLibraryOverlayPaths, that.mSharedLibraryOverlayPaths)
+                && Objects.equals(mSplashScreenTheme, that.mSplashScreenTheme)
+                && Objects.equals(mSuspendParams, that.mSuspendParams)
+                && Objects.equals(mComponentLabelIconOverrideMap, that.mComponentLabelIconOverrideMap)
+                && mFirstInstallTime == that.mFirstInstallTime
+                && watchableEquals(that.mWatchable)
+                && snapshotEquals(that.mSnapshot);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + Objects.hashCode(mDisabledComponentsWatched);
+        _hash = 31 * _hash + Objects.hashCode(mEnabledComponentsWatched);
+        _hash = 31 * _hash + Long.hashCode(mCeDataInode);
+        _hash = 31 * _hash + Boolean.hashCode(mInstalled);
+        _hash = 31 * _hash + Boolean.hashCode(mStopped);
+        _hash = 31 * _hash + Boolean.hashCode(mNotLaunched);
+        _hash = 31 * _hash + Boolean.hashCode(mHidden);
+        _hash = 31 * _hash + mDistractionFlags;
+        _hash = 31 * _hash + Boolean.hashCode(mInstantApp);
+        _hash = 31 * _hash + Boolean.hashCode(mVirtualPreload);
+        _hash = 31 * _hash + mEnabledState;
+        _hash = 31 * _hash + mInstallReason;
+        _hash = 31 * _hash + mUninstallReason;
+        _hash = 31 * _hash + Objects.hashCode(mHarmfulAppWarning);
+        _hash = 31 * _hash + Objects.hashCode(mLastDisableAppCaller);
+        _hash = 31 * _hash + Objects.hashCode(mOverlayPaths);
+        _hash = 31 * _hash + Objects.hashCode(mSharedLibraryOverlayPaths);
+        _hash = 31 * _hash + Objects.hashCode(mSplashScreenTheme);
+        _hash = 31 * _hash + Objects.hashCode(mSuspendParams);
+        _hash = 31 * _hash + Objects.hashCode(mComponentLabelIconOverrideMap);
+        _hash = 31 * _hash + Long.hashCode(mFirstInstallTime);
+        _hash = 31 * _hash + watchableHashCode();
+        _hash = 31 * _hash + snapshotHashCode();
+        return _hash;
+    }
+
     @DataClass.Generated(
-            time = 1645040852569L,
+            time = 1668033772891L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java",
-            inputSignatures = "protected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mDisabledComponentsWatched\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mEnabledComponentsWatched\nprivate  long mCeDataInode\nprivate  boolean mInstalled\nprivate  boolean mStopped\nprivate  boolean mNotLaunched\nprivate  boolean mHidden\nprivate  int mDistractionFlags\nprivate  boolean mInstantApp\nprivate  boolean mVirtualPreload\nprivate  int mEnabledState\nprivate @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate @android.annotation.Nullable android.content.pm.overlay.OverlayPaths mOverlayPaths\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams> mSuspendParams\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>> mComponentLabelIconOverrideMap\nprivate  long mFirstInstallTime\nprivate final @android.annotation.Nullable com.android.server.utils.Watchable mWatchable\nfinal @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> mSnapshot\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> makeCache()\nprivate  void onChanged()\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserStateImpl snapshot()\npublic @android.annotation.Nullable boolean setOverlayPaths(android.content.pm.overlay.OverlayPaths)\npublic  boolean setSharedLibraryOverlayPaths(java.lang.String,android.content.pm.overlay.OverlayPaths)\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponentsNoCopy()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponentsNoCopy()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getDisabledComponents()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getEnabledComponents()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer)\npublic  void resetOverrideComponentLabelIcon()\npublic @android.annotation.Nullable android.util.Pair<java.lang.String,java.lang.Integer> getOverrideLabelIconForComponent(android.content.ComponentName)\npublic @java.lang.Override boolean isSuspended()\npublic  com.android.server.pm.pkg.PackageUserStateImpl putSuspendParams(java.lang.String,com.android.server.pm.pkg.SuspendParams)\npublic  com.android.server.pm.pkg.PackageUserStateImpl removeSuspension(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setCeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstalled(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setStopped(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setNotLaunched(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHidden(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDistractionFlags(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstantApp(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setVirtualPreload(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setUninstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHarmfulAppWarning(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setLastDisableAppCaller(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSharedLibraryOverlayPaths(android.util.ArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSplashScreenTheme(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSuspendParams(android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setComponentLabelIconOverrideMap(android.util.ArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setFirstInstallTime(long)\npublic @android.annotation.NonNull @java.lang.Override java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> getSharedLibraryOverlayPaths()\npublic @java.lang.Override boolean equals(java.lang.Object)\npublic @java.lang.Override int hashCode()\nclass PackageUserStateImpl extends com.android.server.utils.WatchableImpl implements [com.android.server.pm.pkg.PackageUserStateInternal, com.android.server.utils.Snappable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genEqualsHashCode=true)")
+            inputSignatures = "protected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mDisabledComponentsWatched\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mEnabledComponentsWatched\nprivate  long mCeDataInode\nprivate  boolean mInstalled\nprivate  boolean mStopped\nprivate  boolean mNotLaunched\nprivate  boolean mHidden\nprivate  int mDistractionFlags\nprivate  boolean mInstantApp\nprivate  boolean mVirtualPreload\nprivate  int mEnabledState\nprivate @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate @android.annotation.Nullable android.content.pm.overlay.OverlayPaths mOverlayPaths\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams> mSuspendParams\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>> mComponentLabelIconOverrideMap\nprivate  long mFirstInstallTime\nprivate @android.annotation.Nullable com.android.server.utils.Watchable mWatchable\nfinal @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> mSnapshot\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> makeCache()\nprivate  void onChanged()\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserStateImpl snapshot()\npublic @android.annotation.Nullable boolean setOverlayPaths(android.content.pm.overlay.OverlayPaths)\npublic  boolean setSharedLibraryOverlayPaths(java.lang.String,android.content.pm.overlay.OverlayPaths)\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponentsNoCopy()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponentsNoCopy()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getDisabledComponents()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getEnabledComponents()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer)\npublic  void resetOverrideComponentLabelIcon()\npublic @android.annotation.Nullable android.util.Pair<java.lang.String,java.lang.Integer> getOverrideLabelIconForComponent(android.content.ComponentName)\npublic @java.lang.Override boolean isSuspended()\npublic  com.android.server.pm.pkg.PackageUserStateImpl putSuspendParams(java.lang.String,com.android.server.pm.pkg.SuspendParams)\npublic  com.android.server.pm.pkg.PackageUserStateImpl removeSuspension(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setCeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstalled(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setStopped(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setNotLaunched(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHidden(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDistractionFlags(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstantApp(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setVirtualPreload(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setUninstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHarmfulAppWarning(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setLastDisableAppCaller(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSharedLibraryOverlayPaths(android.util.ArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSplashScreenTheme(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSuspendParams(android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setComponentLabelIconOverrideMap(android.util.ArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setFirstInstallTime(long)\npublic @android.annotation.NonNull @java.lang.Override java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> getSharedLibraryOverlayPaths()\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setWatchable(com.android.server.utils.Watchable)\nprivate  boolean watchableEquals(com.android.server.utils.Watchable)\nprivate  int watchableHashCode()\nprivate  boolean snapshotEquals(com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl>)\nprivate  int snapshotHashCode()\nclass PackageUserStateImpl extends com.android.server.utils.WatchableImpl implements [com.android.server.pm.pkg.PackageUserStateInternal, com.android.server.utils.Snappable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genEqualsHashCode=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
index e736f43..4a8ef96 100644
--- a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
+++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
@@ -40,6 +40,8 @@
     private final Function<String, PackageSetting> mActiveStateFunction;
     private final Function<String, PackageSetting> mDisabledStateFunction;
 
+    private final ArraySet<PackageSetting> mChangedStates = new ArraySet<>();
+
     public PackageStateMutator(@NonNull Function<String, PackageSetting> activeStateFunction,
             @NonNull Function<String, PackageSetting> disabledStateFunction) {
         mActiveStateFunction = activeStateFunction;
@@ -52,23 +54,23 @@
 
     @NonNull
     public PackageStateWrite forPackage(@NonNull String packageName) {
-        return mStateWrite.setState(mActiveStateFunction.apply(packageName));
+        return setState(mActiveStateFunction.apply(packageName));
     }
 
     @Nullable
     public PackageStateWrite forPackageNullable(@NonNull String packageName) {
         final PackageSetting packageState = mActiveStateFunction.apply(packageName);
-        mStateWrite.setState(packageState);
+        setState(packageState);
         if (packageState == null) {
             return null;
         }
 
-        return mStateWrite.setState(packageState);
+        return setState(packageState);
     }
 
     @NonNull
     public PackageStateWrite forDisabledSystemPackage(@NonNull String packageName) {
-        return mStateWrite.setState(mDisabledStateFunction.apply(packageName));
+        return setState(mDisabledStateFunction.apply(packageName));
     }
 
     @Nullable
@@ -78,7 +80,7 @@
             return null;
         }
 
-        return mStateWrite.setState(packageState);
+        return setState(packageState);
     }
 
     @NonNull
@@ -109,6 +111,21 @@
         }
     }
 
+    public void onFinished() {
+        for (int index = 0; index < mChangedStates.size(); index++) {
+            mChangedStates.valueAt(index).onChanged();
+        }
+    }
+
+    @NonNull
+    private StateWriteWrapper setState(@Nullable PackageSetting state) {
+        // State can be nullable because this infrastructure no-ops on non-existent states
+        if (state != null) {
+            mChangedStates.add(state);
+        }
+        return mStateWrite.setState(state);
+    }
+
     public static class InitialState {
 
         private final int mPackageSequence;
@@ -173,8 +190,11 @@
         @NonNull
         @Override
         public PackageUserStateWrite userState(int userId) {
-            return mUserStateWrite.setStates(
-                    mState == null ? null : mState.getOrCreateUserState(userId));
+            var userState = mState == null ? null : mState.getOrCreateUserState(userId);
+            if (userState != null) {
+                userState.setWatchable(mState);
+            }
+            return mUserStateWrite.setStates(userState);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index d2e0502..91bb677 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -378,13 +378,14 @@
                 try {
                     conditionSatisfied = mStateConditions.get(state).getAsBoolean();
                 } catch (IllegalStateException e) {
-                    // Failed to compute the current state based on current available data. Return
+                    // Failed to compute the current state based on current available data. Continue
                     // with the expectation that notifyDeviceStateChangedIfNeeded() will be called
-                    // when a callback with the missing data is triggered.
+                    // when a callback with the missing data is triggered. May trigger another state
+                    // change if another state is satisfied currently.
                     if (DEBUG) {
                         Slog.d(TAG, "Unable to check current state", e);
                     }
-                    return;
+                    continue;
                 }
 
                 if (conditionSatisfied) {
diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java
index e61effa..d6cac33 100644
--- a/services/core/java/com/android/server/policy/PermissionPolicyService.java
+++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java
@@ -232,7 +232,7 @@
             }
         };
 
-        final ArrayList<PermissionInfo> dangerousPerms =
+        final List<PermissionInfo> dangerousPerms =
                 mPermissionManagerInternal.getAllPermissionsWithProtection(
                         PermissionInfo.PROTECTION_DANGEROUS);
         try {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index e9c93ee..2f0f88a 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -151,6 +151,7 @@
 import android.provider.DeviceConfig;
 import android.provider.MediaStore;
 import android.provider.Settings;
+import android.provider.Settings.Secure;
 import android.service.dreams.DreamManagerInternal;
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamManager;
@@ -516,6 +517,7 @@
     int mDoublePressOnStemPrimaryBehavior;
     int mTriplePressOnStemPrimaryBehavior;
     int mLongPressOnStemPrimaryBehavior;
+    boolean mStylusButtonsDisabled = false;
     boolean mHasSoftInput = false;
     boolean mHapticTextHandleEnabled;
     boolean mUseTvRouting;
@@ -771,6 +773,9 @@
             resolver.registerContentObserver(Settings.Global.getUriFor(
                     Settings.Global.POWER_BUTTON_SUPPRESSION_DELAY_AFTER_GESTURE_WAKE), false, this,
                     UserHandle.USER_ALL);
+            resolver.registerContentObserver(Settings.Secure.getUriFor(
+                    Settings.Secure.STYLUS_BUTTONS_DISABLED), false, this,
+                    UserHandle.USER_ALL);
             updateSettings();
         }
 
@@ -2560,6 +2565,9 @@
                     Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
                     mContext.getResources().getInteger(
                             com.android.internal.R.integer.config_keyChordPowerVolumeUp));
+
+            mStylusButtonsDisabled = Settings.Secure.getIntForUser(resolver,
+                    Secure.STYLUS_BUTTONS_DISABLED, 0, UserHandle.USER_CURRENT) == 1;
         }
         if (updateRotation) {
             updateRotation(true);
@@ -2912,6 +2920,27 @@
                     return key_consumed;
                 }
                 break;
+            case KeyEvent.KEYCODE_DPAD_UP:
+                if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+                    StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
+                    if (statusbar != null) {
+                        statusbar.goToFullscreenFromSplit();
+                    }
+                    return key_consumed;
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+                    enterStageSplitFromRunningApp(true /* leftOrTop */);
+                    return key_consumed;
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+                    enterStageSplitFromRunningApp(false /* leftOrTop */);
+                    return key_consumed;
+                }
+                break;
             case KeyEvent.KEYCODE_SLASH:
                 if (down && repeatCount == 0 && event.isMetaPressed() && !keyguardOn) {
                     toggleKeyboardShortcutsMenu(event.getDeviceId());
@@ -3566,6 +3595,13 @@
         }
     }
 
+    private void enterStageSplitFromRunningApp(boolean leftOrTop) {
+        StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
+        if (statusbar != null) {
+            statusbar.enterStageSplitFromRunningApp(leftOrTop);
+        }
+    }
+
     void launchHomeFromHotKey(int displayId) {
         launchHomeFromHotKey(displayId, true /* awakenFromDreams */, true /*respectKeyguard*/);
     }
@@ -4183,7 +4219,9 @@
             case KeyEvent.KEYCODE_DEMO_APP_3:
             case KeyEvent.KEYCODE_DEMO_APP_4: {
                 // TODO(b/254604589): Dispatch KeyEvent to System UI.
-                sendSystemKeyToStatusBarAsync(keyCode);
+                if (!mStylusButtonsDisabled) {
+                    sendSystemKeyToStatusBarAsync(keyCode);
+                }
 
                 // Just drop if keys are not intercepted for direct key.
                 result &= ~ACTION_PASS_TO_USER;
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 0c5e451..af4fa85 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -12229,6 +12229,7 @@
     @GuardedBy("this")
     private void incrementPerRatDataLocked(ModemActivityInfo deltaInfo, long elapsedRealtimeMs) {
         final int infoSize = deltaInfo.getSpecificInfoLength();
+
         if (infoSize == 1 && deltaInfo.getSpecificInfoRat(0)
                 == AccessNetworkConstants.AccessNetworkType.UNKNOWN
                 && deltaInfo.getSpecificInfoFrequencyRange(0)
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
index 806ed64..2c7aea9 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
@@ -15,45 +15,79 @@
  */
 package com.android.server.power.stats;
 
+import android.annotation.Nullable;
 import android.os.BatteryConsumer;
 import android.os.BatteryStats;
 import android.os.BatteryUsageStats;
 import android.os.BatteryUsageStatsQuery;
 import android.os.UidBatteryConsumer;
 import android.telephony.CellSignalStrength;
+import android.telephony.ServiceState;
 import android.util.Log;
+import android.util.LongArrayQueue;
 import android.util.SparseArray;
 
 import com.android.internal.os.PowerProfile;
+import com.android.internal.power.ModemPowerProfile;
+
+import java.util.ArrayList;
 
 public class MobileRadioPowerCalculator extends PowerCalculator {
     private static final String TAG = "MobRadioPowerCalculator";
     private static final boolean DEBUG = PowerCalculator.DEBUG;
 
+    private static final double MILLIS_IN_HOUR = 1000.0 * 60 * 60;
+
     private static final int NUM_SIGNAL_STRENGTH_LEVELS =
             CellSignalStrength.getNumSignalStrengthLevels();
 
     private static final BatteryConsumer.Key[] UNINITIALIZED_KEYS = new BatteryConsumer.Key[0];
+    private static final int IGNORE = -1;
 
-    private final UsageBasedPowerEstimator mActivePowerEstimator;
+    private final UsageBasedPowerEstimator mActivePowerEstimator; // deprecated
     private final UsageBasedPowerEstimator[] mIdlePowerEstimators =
-            new UsageBasedPowerEstimator[NUM_SIGNAL_STRENGTH_LEVELS];
-    private final UsageBasedPowerEstimator mScanPowerEstimator;
+            new UsageBasedPowerEstimator[NUM_SIGNAL_STRENGTH_LEVELS]; // deprecated
+    private final UsageBasedPowerEstimator mScanPowerEstimator; // deprecated
+
+    @Nullable
+    private final UsageBasedPowerEstimator mSleepPowerEstimator;
+    @Nullable
+    private final UsageBasedPowerEstimator mIdlePowerEstimator;
+
+    private final PowerProfile mPowerProfile;
 
     private static class PowerAndDuration {
-        public long durationMs;
+        public long remainingDurationMs;
         public double remainingPowerMah;
         public long totalAppDurationMs;
         public double totalAppPowerMah;
-        public long signalDurationMs;
-        public long noCoverageDurationMs;
     }
 
     public MobileRadioPowerCalculator(PowerProfile profile) {
-        // Power consumption when radio is active
+        mPowerProfile = profile;
+
+        final double sleepDrainRateMa = mPowerProfile.getAverageBatteryDrainOrDefaultMa(
+                PowerProfile.SUBSYSTEM_MODEM | ModemPowerProfile.MODEM_DRAIN_TYPE_SLEEP,
+                Double.NaN);
+        if (Double.isNaN(sleepDrainRateMa)) {
+            mSleepPowerEstimator = null;
+        } else {
+            mSleepPowerEstimator = new UsageBasedPowerEstimator(sleepDrainRateMa);
+        }
+
+        final double idleDrainRateMa = mPowerProfile.getAverageBatteryDrainOrDefaultMa(
+                PowerProfile.SUBSYSTEM_MODEM | ModemPowerProfile.MODEM_DRAIN_TYPE_IDLE,
+                Double.NaN);
+        if (Double.isNaN(idleDrainRateMa)) {
+            mIdlePowerEstimator = null;
+        } else {
+            mIdlePowerEstimator = new UsageBasedPowerEstimator(idleDrainRateMa);
+        }
+
+        // Instantiate legacy power estimators
         double powerRadioActiveMa =
-                profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ACTIVE, -1);
-        if (powerRadioActiveMa == -1) {
+                profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ACTIVE, Double.NaN);
+        if (Double.isNaN(powerRadioActiveMa)) {
             double sum = 0;
             sum += profile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX);
             for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
@@ -61,11 +95,10 @@
             }
             powerRadioActiveMa = sum / (NUM_SIGNAL_STRENGTH_LEVELS + 1);
         }
-
         mActivePowerEstimator = new UsageBasedPowerEstimator(powerRadioActiveMa);
 
-        // Power consumption when radio is on, but idle
-        if (profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ON, -1) != -1) {
+        if (!Double.isNaN(
+                profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ON, Double.NaN))) {
             for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
                 mIdlePowerEstimators[i] = new UsageBasedPowerEstimator(
                         profile.getAveragePower(PowerProfile.POWER_RADIO_ON, i));
@@ -95,6 +128,23 @@
 
         PowerAndDuration total = new PowerAndDuration();
 
+        final long totalConsumptionUC = batteryStats.getMobileRadioMeasuredBatteryConsumptionUC();
+        final int powerModel = getPowerModel(totalConsumptionUC, query);
+
+        final double totalActivePowerMah;
+        final ArrayList<UidBatteryConsumer.Builder> apps;
+        final LongArrayQueue appDurationsMs;
+        if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
+            // Measured energy is available, don't bother calculating power.
+            totalActivePowerMah = Double.NaN;
+            apps = null;
+            appDurationsMs = null;
+        } else {
+            totalActivePowerMah = calculateActiveModemPowerMah(batteryStats, rawRealtimeUs);
+            apps = new ArrayList();
+            appDurationsMs = new LongArrayQueue();
+        }
+
         final SparseArray<UidBatteryConsumer.Builder> uidBatteryConsumerBuilders =
                 builder.getUidBatteryConsumerBuilders();
         BatteryConsumer.Key[] keys = UNINITIALIZED_KEYS;
@@ -110,132 +160,352 @@
                 }
             }
 
-            calculateApp(app, uid, total, query, keys);
+            // Sum and populate each app's active radio duration.
+            final long radioActiveDurationMs = calculateDuration(uid,
+                    BatteryStats.STATS_SINCE_CHARGED);
+            if (!app.isVirtualUid()) {
+                total.totalAppDurationMs += radioActiveDurationMs;
+            }
+            app.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
+                    radioActiveDurationMs);
+
+            if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
+                // Measured energy is available, populate the consumed power now.
+                final long appConsumptionUC = uid.getMobileRadioMeasuredBatteryConsumptionUC();
+                if (appConsumptionUC != BatteryStats.POWER_DATA_UNAVAILABLE) {
+                    final double appConsumptionMah = uCtoMah(appConsumptionUC);
+                    if (!app.isVirtualUid()) {
+                        total.totalAppPowerMah += appConsumptionMah;
+                    }
+                    app.setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
+                            appConsumptionMah, powerModel);
+
+                    if (query.isProcessStateDataNeeded() && keys != null) {
+                        for (BatteryConsumer.Key key : keys) {
+                            final int processState = key.processState;
+                            if (processState == BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+                                // Already populated with the total across all process states
+                                continue;
+                            }
+                            final long consumptionInStateUc =
+                                    uid.getMobileRadioMeasuredBatteryConsumptionUC(processState);
+                            final double powerInStateMah = uCtoMah(consumptionInStateUc);
+                            app.setConsumedPower(key, powerInStateMah, powerModel);
+                        }
+                    }
+                }
+            } else {
+                // Cache the app and its active duration for later calculations.
+                apps.add(app);
+                appDurationsMs.addLast(radioActiveDurationMs);
+            }
         }
 
-        final long totalConsumptionUC = batteryStats.getMobileRadioMeasuredBatteryConsumptionUC();
-        final int powerModel = getPowerModel(totalConsumptionUC, query);
-        calculateRemaining(total, powerModel, batteryStats, rawRealtimeUs, totalConsumptionUC);
+        long totalActiveDurationMs = batteryStats.getMobileRadioActiveTime(rawRealtimeUs,
+                BatteryStats.STATS_SINCE_CHARGED) / 1000;
+        if (totalActiveDurationMs < total.totalAppDurationMs) {
+            totalActiveDurationMs = total.totalAppDurationMs;
+        }
+
+        if (powerModel != BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
+            // Need to smear the calculated total active power across the apps based on app
+            // active durations.
+            final int appSize = apps.size();
+            for (int i = 0; i < appSize; i++) {
+                final UidBatteryConsumer.Builder app = apps.get(i);
+                final long activeDurationMs = appDurationsMs.get(i);
+
+                // Proportionally attribute radio power consumption based on active duration.
+                final double appConsumptionMah;
+                if (totalActiveDurationMs == 0.0) {
+                    appConsumptionMah = 0.0;
+                } else {
+                    appConsumptionMah =
+                            (totalActivePowerMah * activeDurationMs) / totalActiveDurationMs;
+                }
+
+                if (!app.isVirtualUid()) {
+                    total.totalAppPowerMah += appConsumptionMah;
+                }
+                app.setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
+                        appConsumptionMah, powerModel);
+
+                if (query.isProcessStateDataNeeded() && keys != null) {
+                    final BatteryStats.Uid uid = app.getBatteryStatsUid();
+                    for (BatteryConsumer.Key key : keys) {
+                        final int processState = key.processState;
+                        if (processState == BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+                            // Already populated with the total across all process states
+                            continue;
+                        }
+
+                        final long durationInStateMs =
+                                uid.getMobileRadioActiveTimeInProcessState(processState) / 1000;
+                        // Proportionally attribute per process state radio power consumption
+                        // based on time state duration.
+                        final double powerInStateMah;
+                        if (activeDurationMs == 0.0) {
+                            powerInStateMah = 0.0;
+                        } else {
+                            powerInStateMah =
+                                    (appConsumptionMah * durationInStateMs) / activeDurationMs;
+                        }
+                        app.setConsumedPower(key, powerInStateMah, powerModel);
+                    }
+                }
+            }
+        }
+
+        total.remainingDurationMs = totalActiveDurationMs - total.totalAppDurationMs;
+
+        // Calculate remaining power consumption.
+        if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
+            total.remainingPowerMah = uCtoMah(totalConsumptionUC) - total.totalAppPowerMah;
+            if (total.remainingPowerMah < 0) total.remainingPowerMah = 0;
+        } else {
+            // Smear unattributed active time and add it to the remaining power consumption.
+            total.remainingPowerMah +=
+                    (totalActivePowerMah * total.remainingDurationMs) / totalActiveDurationMs;
+
+            // Calculate the inactive modem power consumption.
+            final BatteryStats.ControllerActivityCounter modemActivity =
+                    batteryStats.getModemControllerActivity();
+            if (modemActivity != null && (mSleepPowerEstimator != null
+                    || mIdlePowerEstimator != null)) {
+                final long sleepDurationMs = modemActivity.getSleepTimeCounter().getCountLocked(
+                        BatteryStats.STATS_SINCE_CHARGED);
+                total.remainingPowerMah += mSleepPowerEstimator.calculatePower(sleepDurationMs);
+                final long idleDurationMs = modemActivity.getIdleTimeCounter().getCountLocked(
+                        BatteryStats.STATS_SINCE_CHARGED);
+                total.remainingPowerMah += mIdlePowerEstimator.calculatePower(idleDurationMs);
+            } else {
+                // Modem activity counters unavailable. Use legacy calculations for inactive usage.
+                final long scanningTimeMs = batteryStats.getPhoneSignalScanningTime(rawRealtimeUs,
+                        BatteryStats.STATS_SINCE_CHARGED) / 1000;
+                total.remainingPowerMah += calcScanTimePowerMah(scanningTimeMs);
+                for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
+                    long strengthTimeMs = batteryStats.getPhoneSignalStrengthTime(i, rawRealtimeUs,
+                            BatteryStats.STATS_SINCE_CHARGED) / 1000;
+                    total.remainingPowerMah += calcIdlePowerAtSignalStrengthMah(strengthTimeMs, i);
+                }
+            }
+
+        }
 
         if (total.remainingPowerMah != 0 || total.totalAppPowerMah != 0) {
             builder.getAggregateBatteryConsumerBuilder(
-                    BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+                            BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
                     .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
-                            total.durationMs)
+                            total.remainingDurationMs + total.totalAppDurationMs)
                     .setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
                             total.remainingPowerMah + total.totalAppPowerMah, powerModel);
 
             builder.getAggregateBatteryConsumerBuilder(
-                    BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
+                            BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
                     .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
-                            total.durationMs)
+                            total.totalAppDurationMs)
                     .setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
                             total.totalAppPowerMah, powerModel);
         }
     }
 
-    private void calculateApp(UidBatteryConsumer.Builder app, BatteryStats.Uid u,
-            PowerAndDuration total,
-            BatteryUsageStatsQuery query, BatteryConsumer.Key[] keys) {
-        final long radioActiveDurationMs = calculateDuration(u, BatteryStats.STATS_SINCE_CHARGED);
-        final long consumptionUC = u.getMobileRadioMeasuredBatteryConsumptionUC();
-        final int powerModel = getPowerModel(consumptionUC, query);
-        final double powerMah = calculatePower(u, powerModel, radioActiveDurationMs, consumptionUC);
-
-        if (!app.isVirtualUid()) {
-            total.totalAppDurationMs += radioActiveDurationMs;
-            total.totalAppPowerMah += powerMah;
-        }
-
-        app.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
-                        radioActiveDurationMs)
-                .setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, powerMah,
-                        powerModel);
-
-        if (query.isProcessStateDataNeeded() && keys != null) {
-            for (BatteryConsumer.Key key: keys) {
-                final int processState = key.processState;
-                if (processState == BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
-                    // Already populated with the total across all process states
-                    continue;
-                }
-
-                final long durationInStateMs =
-                        u.getMobileRadioActiveTimeInProcessState(processState) / 1000;
-                final long consumptionInStateUc =
-                        u.getMobileRadioMeasuredBatteryConsumptionUC(processState);
-                final double powerInStateMah = calculatePower(u, powerModel, durationInStateMs,
-                        consumptionInStateUc);
-                app.setConsumedPower(key, powerInStateMah, powerModel);
-            }
-        }
-    }
-
     private long calculateDuration(BatteryStats.Uid u, int statsType) {
         return u.getMobileRadioActiveTime(statsType) / 1000;
     }
 
-    private double calculatePower(BatteryStats.Uid u, @BatteryConsumer.PowerModel int powerModel,
-            long radioActiveDurationMs, long measuredChargeUC) {
-        if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
-            return uCtoMah(measuredChargeUC);
+    private double calculateActiveModemPowerMah(BatteryStats bs, long elapsedRealtimeUs) {
+        final long elapsedRealtimeMs = elapsedRealtimeUs / 1000;
+        final int txLvlCount = CellSignalStrength.getNumSignalStrengthLevels();
+        double consumptionMah = 0.0;
+
+        if (DEBUG) {
+            Log.d(TAG, "Calculating radio power consumption at elapased real timestamp : "
+                    + elapsedRealtimeMs + " ms");
         }
 
-        if (radioActiveDurationMs > 0) {
-            return calcPowerFromRadioActiveDurationMah(radioActiveDurationMs);
+        boolean hasConstants = false;
+
+        for (int rat = 0; rat < BatteryStats.RADIO_ACCESS_TECHNOLOGY_COUNT; rat++) {
+            final int freqCount = rat == BatteryStats.RADIO_ACCESS_TECHNOLOGY_NR
+                    ? ServiceState.FREQUENCY_RANGE_COUNT : 1;
+            for (int freq = 0; freq < freqCount; freq++) {
+                for (int txLvl = 0; txLvl < txLvlCount; txLvl++) {
+                    final long txDurationMs = bs.getActiveTxRadioDurationMs(rat, freq, txLvl,
+                            elapsedRealtimeMs);
+                    if (txDurationMs == BatteryStats.DURATION_UNAVAILABLE) {
+                        continue;
+                    }
+                    final double txConsumptionMah = calcTxStatePowerMah(rat, freq, txLvl,
+                            txDurationMs);
+                    if (Double.isNaN(txConsumptionMah)) {
+                        continue;
+                    }
+                    hasConstants = true;
+                    consumptionMah += txConsumptionMah;
+                }
+
+                final long rxDurationMs = bs.getActiveRxRadioDurationMs(rat, freq,
+                        elapsedRealtimeMs);
+                if (rxDurationMs == BatteryStats.DURATION_UNAVAILABLE) {
+                    continue;
+                }
+                final double rxConsumptionMah = calcRxStatePowerMah(rat, freq, rxDurationMs);
+                if (Double.isNaN(rxConsumptionMah)) {
+                    continue;
+                }
+                hasConstants = true;
+                consumptionMah += rxConsumptionMah;
+            }
         }
-        return 0;
+
+        if (!hasConstants) {
+            final long radioActiveDurationMs = bs.getMobileRadioActiveTime(elapsedRealtimeUs,
+                    BatteryStats.STATS_SINCE_CHARGED) / 1000;
+            if (DEBUG) {
+                Log.d(TAG,
+                        "Failed to calculate radio power consumption. Reattempted with legacy "
+                                + "method. Radio active duration : "
+                                + radioActiveDurationMs + " ms");
+            }
+            if (radioActiveDurationMs > 0) {
+                consumptionMah = calcPowerFromRadioActiveDurationMah(radioActiveDurationMs);
+            } else {
+                consumptionMah = 0.0;
+            }
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Total active radio power consumption calculated to be " + consumptionMah
+                    + " mAH.");
+        }
+
+        return consumptionMah;
     }
 
-    private void calculateRemaining(PowerAndDuration total,
-            @BatteryConsumer.PowerModel int powerModel, BatteryStats batteryStats,
-            long rawRealtimeUs, long totalConsumptionUC) {
-        long signalTimeMs = 0;
-        double powerMah = 0;
+    private static long buildModemPowerProfileKey(@ModemPowerProfile.ModemDrainType int drainType,
+            @BatteryStats.RadioAccessTechnology int rat, @ServiceState.FrequencyRange int freqRange,
+            int txLevel) {
+        long key = PowerProfile.SUBSYSTEM_MODEM;
 
-        if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
-            powerMah = uCtoMah(totalConsumptionUC) - total.totalAppPowerMah;
-            if (powerMah < 0) powerMah = 0;
+        // Attach Modem drain type to the key if specified.
+        if (drainType != IGNORE) {
+            key |= drainType;
         }
 
-        for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
-            long strengthTimeMs = batteryStats.getPhoneSignalStrengthTime(i, rawRealtimeUs,
-                    BatteryStats.STATS_SINCE_CHARGED) / 1000;
-            if (powerModel == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
-                final double p = calcIdlePowerAtSignalStrengthMah(strengthTimeMs, i);
-                if (DEBUG && p != 0) {
-                    Log.d(TAG, "Cell strength #" + i + ": time=" + strengthTimeMs + " power="
-                            + BatteryStats.formatCharge(p));
-                }
-                powerMah += p;
-            }
-            signalTimeMs += strengthTimeMs;
-            if (i == 0) {
-                total.noCoverageDurationMs = strengthTimeMs;
-            }
+        // Attach RadioAccessTechnology to the key if specified.
+        switch (rat) {
+            case IGNORE:
+                // do nothing
+                break;
+            case BatteryStats.RADIO_ACCESS_TECHNOLOGY_OTHER:
+                key |= ModemPowerProfile.MODEM_RAT_TYPE_DEFAULT;
+                break;
+            case BatteryStats.RADIO_ACCESS_TECHNOLOGY_LTE:
+                key |= ModemPowerProfile.MODEM_RAT_TYPE_LTE;
+                break;
+            case BatteryStats.RADIO_ACCESS_TECHNOLOGY_NR:
+                key |= ModemPowerProfile.MODEM_RAT_TYPE_NR;
+                break;
+            default:
+                Log.w(TAG, "Unexpected RadioAccessTechnology : " + rat);
         }
 
-        final long scanningTimeMs = batteryStats.getPhoneSignalScanningTime(rawRealtimeUs,
-                BatteryStats.STATS_SINCE_CHARGED) / 1000;
-        long radioActiveTimeMs = batteryStats.getMobileRadioActiveTime(rawRealtimeUs,
-                BatteryStats.STATS_SINCE_CHARGED) / 1000;
-        long remainingActiveTimeMs = radioActiveTimeMs - total.totalAppDurationMs;
-
-        if (powerModel == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
-            final double p = calcScanTimePowerMah(scanningTimeMs);
-            if (DEBUG && p != 0) {
-                Log.d(TAG, "Cell radio scanning: time=" + scanningTimeMs
-                        + " power=" + BatteryStats.formatCharge(p));
-            }
-            powerMah += p;
-
-            if (remainingActiveTimeMs > 0) {
-                powerMah += calcPowerFromRadioActiveDurationMah(remainingActiveTimeMs);
-            }
+        // Attach NR Frequency Range to the key if specified.
+        switch (freqRange) {
+            case IGNORE:
+                // do nothing
+                break;
+            case ServiceState.FREQUENCY_RANGE_UNKNOWN:
+                key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_DEFAULT;
+                break;
+            case ServiceState.FREQUENCY_RANGE_LOW:
+                key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_LOW;
+                break;
+            case ServiceState.FREQUENCY_RANGE_MID:
+                key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_MID;
+                break;
+            case ServiceState.FREQUENCY_RANGE_HIGH:
+                key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_HIGH;
+                break;
+            case ServiceState.FREQUENCY_RANGE_MMWAVE:
+                key |= ModemPowerProfile.MODEM_NR_FREQUENCY_RANGE_MMWAVE;
+                break;
+            default:
+                Log.w(TAG, "Unexpected NR frequency range : " + freqRange);
         }
-        total.durationMs = radioActiveTimeMs;
-        total.remainingPowerMah = powerMah;
-        total.signalDurationMs = signalTimeMs;
+
+        // Attach transmission level to the key if specified.
+        switch (txLevel) {
+            case IGNORE:
+                // do nothing
+                break;
+            case 0:
+                key |= ModemPowerProfile.MODEM_TX_LEVEL_0;
+                break;
+            case 1:
+                key |= ModemPowerProfile.MODEM_TX_LEVEL_1;
+                break;
+            case 2:
+                key |= ModemPowerProfile.MODEM_TX_LEVEL_2;
+                break;
+            case 3:
+                key |= ModemPowerProfile.MODEM_TX_LEVEL_3;
+                break;
+            case 4:
+                key |= ModemPowerProfile.MODEM_TX_LEVEL_4;
+                break;
+            default:
+                Log.w(TAG, "Unexpected transmission level : " + txLevel);
+        }
+        return key;
+    }
+
+    /**
+     * Calculates active receive radio power consumption (in milliamp-hours) from the given state's
+     * duration.
+     */
+    public double calcRxStatePowerMah(@BatteryStats.RadioAccessTechnology int rat,
+            @ServiceState.FrequencyRange int freqRange, long rxDurationMs) {
+        final long rxKey = buildModemPowerProfileKey(ModemPowerProfile.MODEM_DRAIN_TYPE_RX, rat,
+                freqRange, IGNORE);
+        final double drainRateMa = mPowerProfile.getAverageBatteryDrainOrDefaultMa(rxKey,
+                Double.NaN);
+        if (Double.isNaN(drainRateMa)) {
+            Log.w(TAG, "Unavailable Power Profile constant for key 0x" + Long.toHexString(rxKey));
+            return Double.NaN;
+        }
+
+        final double consumptionMah = drainRateMa * rxDurationMs / MILLIS_IN_HOUR;
+        if (DEBUG) {
+            Log.d(TAG, "Calculated RX consumption " + consumptionMah + " mAH from a drain rate of "
+                    + drainRateMa + " mA and a duration of " + rxDurationMs + " ms for "
+                    + ModemPowerProfile.keyToString((int) rxKey));
+        }
+        return consumptionMah;
+    }
+
+    /**
+     * Calculates active transmit radio power consumption (in milliamp-hours) from the given state's
+     * duration.
+     */
+    public double calcTxStatePowerMah(@BatteryStats.RadioAccessTechnology int rat,
+            @ServiceState.FrequencyRange int freqRange, int txLevel, long txDurationMs) {
+        final long txKey = buildModemPowerProfileKey(ModemPowerProfile.MODEM_DRAIN_TYPE_TX, rat,
+                freqRange, txLevel);
+        final double drainRateMa = mPowerProfile.getAverageBatteryDrainOrDefaultMa(txKey,
+                Double.NaN);
+        if (Double.isNaN(drainRateMa)) {
+            Log.w(TAG, "Unavailable Power Profile constant for key 0x" + Long.toHexString(txKey));
+            return Double.NaN;
+        }
+
+        final double consumptionMah = drainRateMa * txDurationMs / MILLIS_IN_HOUR;
+        if (DEBUG) {
+            Log.d(TAG, "Calculated TX consumption " + consumptionMah + " mAH from a drain rate of "
+                    + drainRateMa + " mA and a duration of " + txDurationMs + " ms for "
+                    + ModemPowerProfile.keyToString((int) txKey));
+        }
+        return consumptionMah;
     }
 
     /**
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 434cd78..392fda9 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -205,4 +205,16 @@
      * @see com.android.internal.statusbar.IStatusBar#showRearDisplayDialog
      */
     void showRearDisplayDialog(int currentBaseState);
+
+    /**
+     * Called when requested to go to fullscreen from the active split app.
+     */
+    void goToFullscreenFromSplit();
+
+    /**
+     * Enters stage split from a current running app.
+     *
+     * @see com.android.internal.statusbar.IStatusBar#enterStageSplitFromRunningApp
+     */
+    void enterStageSplitFromRunningApp(boolean leftOrTop);
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 006d888..8d71d9c 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -726,6 +726,24 @@
                 } catch (RemoteException ex) { }
             }
         }
+
+        @Override
+        public void goToFullscreenFromSplit() {
+            if (mBar != null) {
+                try {
+                    mBar.goToFullscreenFromSplit();
+                } catch (RemoteException ex) { }
+            }
+        }
+
+        @Override
+        public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+            if (mBar != null) {
+                try {
+                    mBar.enterStageSplitFromRunningApp(leftOrTop);
+                } catch (RemoteException ex) { }
+            }
+        }
     };
 
     private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() {
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index e155a06..707d0044 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -161,6 +161,32 @@
         return null;
     }
 
+    // Used in testing.
+    void provideDataStream(@UserIdInt int userId, ParcelFileDescriptor parcelFileDescriptor,
+            RemoteCallback callback) {
+        synchronized (mLock) {
+            final WearableSensingManagerPerUserService mService = getServiceForUserLocked(userId);
+            if (mService != null) {
+                mService.onProvideDataStream(parcelFileDescriptor, callback);
+            } else {
+                Slog.w(TAG, "Service not available.");
+            }
+        }
+    }
+
+    // Used in testing.
+    void provideData(@UserIdInt int userId, PersistableBundle data, SharedMemory sharedMemory,
+            RemoteCallback callback) {
+        synchronized (mLock) {
+            final WearableSensingManagerPerUserService mService = getServiceForUserLocked(userId);
+            if (mService != null) {
+                mService.onProvidedData(data, sharedMemory, callback);
+            } else {
+                Slog.w(TAG, "Service not available.");
+            }
+        }
+    }
+
     private final class WearableSensingManagerInternal extends IWearableSensingManager.Stub {
         final WearableSensingManagerPerUserService mService = getServiceForUserLocked(
                 UserHandle.getCallingUserId());
@@ -205,6 +231,8 @@
         @Override
         public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
                 String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
+            new WearableSensingShellCommand(WearableSensingManagerService.this).exec(
+                    this, in, out, err, args, callback, resultReceiver);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java b/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java
new file mode 100644
index 0000000..842bccb
--- /dev/null
+++ b/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java
@@ -0,0 +1,212 @@
+/*
+ * 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.wearable;
+
+import android.annotation.NonNull;
+import android.app.wearable.WearableSensingManager;
+import android.content.ComponentName;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.ShellCommand;
+import android.util.Slog;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+final class WearableSensingShellCommand extends ShellCommand {
+    private static final String TAG = WearableSensingShellCommand.class.getSimpleName();
+
+    static final TestableCallbackInternal sTestableCallbackInternal =
+            new TestableCallbackInternal();
+
+    @NonNull
+    private final WearableSensingManagerService mService;
+
+    private static ParcelFileDescriptor[] sPipe;
+
+    WearableSensingShellCommand(@NonNull WearableSensingManagerService service) {
+        mService = service;
+    }
+
+    /** Callbacks for WearableSensingService results used internally for testing. */
+    static class TestableCallbackInternal {
+        private int mLastStatus;
+
+        public int getLastStatus() {
+            return mLastStatus;
+        }
+
+        @NonNull
+        private RemoteCallback createRemoteStatusCallback() {
+            return new RemoteCallback(result -> {
+                int status = result.getInt(WearableSensingManager.STATUS_RESPONSE_BUNDLE_KEY);
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    mLastStatus = status;
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+            });
+        }
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+
+        switch (cmd) {
+            case "create-data-stream":
+                return createDataStream();
+            case "destroy-data-stream":
+                return destroyDataStream();
+            case "provide-data-stream":
+                return provideDataStream();
+            case "write-to-data-stream":
+                return writeToDataStream();
+            case "provide-data":
+                return provideData();
+            case "get-last-status-code":
+                return getLastStatusCode();
+            case "get-bound-package":
+                return getBoundPackageName();
+            case "set-temporary-service":
+                return setTemporaryService();
+            default:
+                return handleDefaultCommands(cmd);
+        }
+    }
+
+    @Override
+    public void onHelp() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("WearableSensingCommands commands: ");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println();
+        pw.println("  create-data-stream: Creates a data stream to be provided.");
+        pw.println("  destroy-data-stream: Destroys a data stream if one was previously created.");
+        pw.println("  provide-data-stream USER_ID: "
+                + "Provides data stream to WearableSensingService.");
+        pw.println("  write-to-data-stream STRING: writes string to data stream.");
+        pw.println("  provide-data USER_ID KEY INTEGER: provide integer as data with key.");
+        pw.println("  get-last-status-code: Prints the latest request status code.");
+        pw.println("  get-bound-package USER_ID:"
+                + "     Print the bound package that implements the service.");
+        pw.println("  set-temporary-service USER_ID [PACKAGE_NAME] [COMPONENT_NAME DURATION]");
+        pw.println("    Temporarily (for DURATION ms) changes the service implementation.");
+        pw.println("    To reset, call with just the USER_ID argument.");
+    }
+
+    private int createDataStream() {
+        Slog.d(TAG, "createDataStream");
+        try {
+            sPipe = ParcelFileDescriptor.createPipe();
+        } catch (IOException e) {
+            Slog.d(TAG, "Failed to createDataStream.", e);
+        }
+        return 0;
+    }
+
+    private int destroyDataStream() {
+        Slog.d(TAG, "destroyDataStream");
+        try {
+            if (sPipe != null) {
+                sPipe[0].close();
+                sPipe[1].close();
+            }
+        } catch (IOException e) {
+            Slog.d(TAG, "Failed to destroyDataStream.", e);
+        }
+        return 0;
+    }
+
+    private int provideDataStream() {
+        Slog.d(TAG, "provideDataStream");
+        if (sPipe != null) {
+            final int userId = Integer.parseInt(getNextArgRequired());
+            mService.provideDataStream(userId, sPipe[0],
+                    sTestableCallbackInternal.createRemoteStatusCallback());
+        }
+        return 0;
+    }
+
+    private int writeToDataStream() {
+        Slog.d(TAG, "writeToDataStream");
+        if (sPipe != null) {
+            final String value = getNextArgRequired();
+            try {
+                ParcelFileDescriptor writePipe = sPipe[1].dup();
+                OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(writePipe);
+                os.write(value.getBytes());
+            } catch (IOException e) {
+                Slog.d(TAG, "Failed to writeToDataStream.", e);
+            }
+        }
+        return 0;
+    }
+
+    private int provideData() {
+        Slog.d(TAG, "provideData");
+        final int userId = Integer.parseInt(getNextArgRequired());
+        final String key = getNextArgRequired();
+        final int value = Integer.parseInt(getNextArgRequired());
+        PersistableBundle data = new PersistableBundle();
+        data.putInt(key, value);
+
+        mService.provideData(userId, data, null,
+                sTestableCallbackInternal.createRemoteStatusCallback());
+        return 0;
+    }
+
+    private int getLastStatusCode() {
+        Slog.d(TAG, "getLastStatusCode");
+        final PrintWriter resultPrinter = getOutPrintWriter();
+        int lastStatus = sTestableCallbackInternal.getLastStatus();
+        resultPrinter.println(lastStatus);
+        return 0;
+    }
+
+    private int setTemporaryService() {
+        final PrintWriter out = getOutPrintWriter();
+        final int userId = Integer.parseInt(getNextArgRequired());
+        final String serviceName = getNextArg();
+        if (serviceName == null) {
+            mService.resetTemporaryService(userId);
+            out.println("WearableSensingManagerService temporary reset. ");
+            return 0;
+        }
+
+        final int duration = Integer.parseInt(getNextArgRequired());
+        mService.setTemporaryService(userId, serviceName, duration);
+        out.println("WearableSensingService temporarily set to " + serviceName
+                + " for " + duration + "ms");
+        return 0;
+    }
+
+    private int getBoundPackageName() {
+        final PrintWriter resultPrinter = getOutPrintWriter();
+        final int userId = Integer.parseInt(getNextArgRequired());
+        final ComponentName componentName = mService.getComponentName(userId);
+        resultPrinter.println(componentName == null ? "" : componentName.getPackageName());
+        return 0;
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index e1ab291..13111fb 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -740,7 +740,7 @@
         // visible such as after the top task is finished.
         for (int i = mTransitionInfoList.size() - 2; i >= 0; i--) {
             final TransitionInfo prevInfo = mTransitionInfoList.get(i);
-            if (prevInfo.mIsDrawn || !prevInfo.mLastLaunchedActivity.mVisibleRequested) {
+            if (prevInfo.mIsDrawn || !prevInfo.mLastLaunchedActivity.isVisibleRequested()) {
                 scheduleCheckActivityToBeDrawn(prevInfo.mLastLaunchedActivity, 0 /* delay */);
             }
         }
@@ -867,7 +867,7 @@
             return;
         }
         if (DEBUG_METRICS) {
-            Slog.i(TAG, "notifyVisibilityChanged " + r + " visible=" + r.mVisibleRequested
+            Slog.i(TAG, "notifyVisibilityChanged " + r + " visible=" + r.isVisibleRequested()
                     + " state=" + r.getState() + " finishing=" + r.finishing);
         }
         if (r.isState(ActivityRecord.State.RESUMED) && r.mDisplayContent.isSleeping()) {
@@ -876,7 +876,7 @@
             // the tracking of launch event.
             return;
         }
-        if (!r.mVisibleRequested || r.finishing) {
+        if (!r.isVisibleRequested() || r.finishing) {
             // Check if the tracker can be cancelled because the last launched activity may be
             // no longer visible.
             scheduleCheckActivityToBeDrawn(r, 0 /* delay */);
@@ -909,7 +909,7 @@
             // activities in this task may be finished, invisible or drawn, so the transition event
             // should be cancelled.
             if (t != null && t.forAllActivities(
-                    a -> a.mVisibleRequested && !a.isReportedDrawn() && !a.finishing)) {
+                    a -> a.isVisibleRequested() && !a.isReportedDrawn() && !a.finishing)) {
                 return;
             }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index e099aac..aec06f0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -800,7 +800,7 @@
     // it will sometimes be true a little earlier: when the activity record has
     // been shown, but is still waiting for its app transition to execute
     // before making its windows shown.
-    boolean mVisibleRequested;
+    private boolean mVisibleRequested;
 
     // Last visibility state we reported to the app token.
     boolean reportedVisible;
@@ -3622,7 +3622,7 @@
         // implied that the current finishing activity should be added into stopping list rather
         // than destroy immediately.
         final boolean isNextNotYetVisible = next != null
-                && (!next.nowVisible || !next.mVisibleRequested);
+                && (!next.nowVisible || !next.isVisibleRequested());
 
         // Clear last paused activity to ensure top activity can be resumed during sleeping.
         if (isNextNotYetVisible && mDisplayContent.isSleeping()
@@ -4440,7 +4440,7 @@
     void transferStartingWindowFromHiddenAboveTokenIfNeeded() {
         task.forAllActivities(fromActivity -> {
             if (fromActivity == this) return true;
-            return !fromActivity.mVisibleRequested && transferStartingWindow(fromActivity);
+            return !fromActivity.isVisibleRequested() && transferStartingWindow(fromActivity);
         });
     }
 
@@ -5105,7 +5105,8 @@
      * This is the only place that writes {@link #mVisibleRequested} (except unit test). The caller
      * outside of this class should use {@link #setVisibility}.
      */
-    private void setVisibleRequested(boolean visible) {
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    void setVisibleRequested(boolean visible) {
         if (visible == mVisibleRequested) {
             return;
         }
@@ -5433,25 +5434,20 @@
      */
     private void postApplyAnimation(boolean visible, boolean fromTransition) {
         final boolean usingShellTransitions = mTransitionController.isShellTransitionsEnabled();
-        final boolean delayed = isAnimating(PARENTS | CHILDREN,
+        final boolean delayed = !usingShellTransitions && isAnimating(PARENTS | CHILDREN,
                 ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION
                         | ANIMATION_TYPE_RECENTS);
-        if (!delayed) {
+        if (!delayed && !usingShellTransitions) {
             // We aren't delayed anything, but exiting windows rely on the animation finished
             // callback being called in case the ActivityRecord was pretending to be delayed,
             // which we might have done because we were in closing/opening apps list.
-            if (!usingShellTransitions) {
-                onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, null /* AnimationAdapter */);
-                if (visible) {
-                    // The token was made immediately visible, there will be no entrance animation.
-                    // We need to inform the client the enter animation was finished.
-                    mEnteringAnimation = true;
-                    mWmService.mActivityManagerAppTransitionNotifier.onAppTransitionFinishedLocked(
-                            token);
-                }
-            } else {
-                // update wallpaper target
-                setAppLayoutChanges(FINISH_LAYOUT_REDO_WALLPAPER, "ActivityRecord");
+            onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, null /* AnimationAdapter */);
+            if (visible) {
+                // The token was made immediately visible, there will be no entrance animation.
+                // We need to inform the client the enter animation was finished.
+                mEnteringAnimation = true;
+                mWmService.mActivityManagerAppTransitionNotifier.onAppTransitionFinishedLocked(
+                        token);
             }
         }
 
@@ -5460,8 +5456,8 @@
         // updated.
         // If we're becoming invisible, update the client visibility if we are not running an
         // animation. Otherwise, we'll update client visibility in onAnimationFinished.
-        if (visible || !isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)
-                || usingShellTransitions) {
+        if (visible || usingShellTransitions
+                || !isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)) {
             setClientVisible(visible);
         }
 
@@ -6549,7 +6545,7 @@
         if (associatedTask == null) {
             removeStartingWindow();
         } else if (associatedTask.getActivity(
-                r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) {
+                r -> r.isVisibleRequested() && !r.firstWindowDrawn) == null) {
             // The last drawn activity may not be the one that owns the starting window.
             final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
             if (r != null) {
@@ -7442,8 +7438,10 @@
             } else if (!show && mLastSurfaceShowing) {
                 getSyncTransaction().hide(mSurfaceControl);
             }
-            if (show) {
-                mActivityRecordInputSink.applyChangesToSurfaceIfChanged(getSyncTransaction());
+            // Input sink surface is not a part of animation, so just apply in a steady state
+            // (non-sync) with pending transaction.
+            if (show && mSyncState == SYNC_STATE_NONE) {
+                mActivityRecordInputSink.applyChangesToSurfaceIfChanged(getPendingTransaction());
             }
         }
         if (mThumbnail != null) {
diff --git a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
index 30c7b23..0859d40 100644
--- a/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
+++ b/services/core/java/com/android/server/wm/ActivityServiceConnectionsHolder.java
@@ -92,7 +92,7 @@
 
     public boolean isActivityVisible() {
         synchronized (mService.mGlobalLock) {
-            return mActivity.mVisibleRequested || mActivity.isState(RESUMED, PAUSING);
+            return mActivity.isVisibleRequested() || mActivity.isState(RESUMED, PAUSING);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 56aae2d6..05ec3b5 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -561,7 +561,7 @@
         if (rootTask == null) return false;
         final RemoteTransition remote = options.getRemoteTransition();
         final ActivityRecord r = rootTask.topRunningActivity();
-        if (r == null || r.mVisibleRequested || !r.attachedToProcess() || remote == null
+        if (r == null || r.isVisibleRequested() || !r.attachedToProcess() || remote == null
                 || !r.mActivityComponent.equals(intent.getComponent())
                 // Recents keeps invisible while device is locked.
                 || r.mDisplayContent.isKeyguardLocked()) {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 5c83c4f..1e06375 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2637,7 +2637,7 @@
                     // If the activity is visible in multi-windowing mode, it may already be on
                     // the top (visible to user but not the global top), then the result code
                     // should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT.
-                    final boolean wasTopOfVisibleRootTask = intentActivity.mVisibleRequested
+                    final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested()
                             && intentActivity.inMultiWindowMode()
                             && intentActivity == mTargetRootTask.topRunningActivity();
                     // We only want to move to the front, if we aren't going to launch on a
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 3145ab3..8a247cf 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -145,6 +145,7 @@
 import com.android.server.am.ActivityManagerService;
 import com.android.server.am.HostingRecord;
 import com.android.server.am.UserState;
+import com.android.server.pm.PackageManagerServiceUtils;
 import com.android.server.utils.Slogf;
 import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
 
@@ -2619,12 +2620,17 @@
         // ActivityStarter will acquire the lock where the places need, so execute the request
         // outside of the lock.
         try {
+            // We need to temporarily disable the explicit intent filter matching enforcement
+            // because Task does not store the resolved type of the intent data, causing filter
+            // mismatch in certain cases. (b/240373119)
+            PackageManagerServiceUtils.DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.set(true);
             return mService.getActivityStartController().startActivityInPackage(taskCallingUid,
                     callingPid, callingUid, callingPackage, callingFeatureId, intent, null, null,
                     null, 0, 0, options, userId, task, "startActivityFromRecents",
                     false /* validateIncomingUser */, null /* originatingPendingIntent */,
                     false /* allowBackgroundActivityStart */);
         } finally {
+            PackageManagerServiceUtils.DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.set(false);
             synchronized (mService.mGlobalLock) {
                 mService.continueWindowLayout();
             }
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 798e739..b16602e 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -168,6 +168,12 @@
                             + "recents. Overriding back callback to recents controller callback.");
                     return null;
                 }
+
+                if (!window.isDrawn()) {
+                    ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
+                            "Focused window didn't have a valid surface drawn.");
+                    return null;
+                }
             }
 
             if (window == null) {
@@ -804,7 +810,7 @@
         if (activity == null) {
             return;
         }
-        if (!activity.mVisibleRequested) {
+        if (!activity.isVisibleRequested()) {
             activity.setVisibility(true);
         }
         activity.mLaunchTaskBehind = true;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 6140508..d5802cf 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -876,11 +876,11 @@
             final ActivityRecord activity = w.mActivityRecord;
             if (gone) Slog.v(TAG, "  GONE: mViewVisibility=" + w.mViewVisibility
                     + " mRelayoutCalled=" + w.mRelayoutCalled + " visible=" + w.mToken.isVisible()
-                    + " visibleRequested=" + (activity != null && activity.mVisibleRequested)
+                    + " visibleRequested=" + (activity != null && activity.isVisibleRequested())
                     + " parentHidden=" + w.isParentWindowHidden());
             else Slog.v(TAG, "  VIS: mViewVisibility=" + w.mViewVisibility
                     + " mRelayoutCalled=" + w.mRelayoutCalled + " visible=" + w.mToken.isVisible()
-                    + " visibleRequested=" + (activity != null && activity.mVisibleRequested)
+                    + " visibleRequested=" + (activity != null && activity.isVisibleRequested())
                     + " parentHidden=" + w.isParentWindowHidden());
         }
 
@@ -1705,7 +1705,7 @@
                         .notifyTaskRequestedOrientationChanged(task.mTaskId, orientation);
             }
             // The orientation source may not be the top if it uses SCREEN_ORIENTATION_BEHIND.
-            final ActivityRecord topCandidate = !r.mVisibleRequested ? topRunningActivity() : r;
+            final ActivityRecord topCandidate = !r.isVisibleRequested() ? topRunningActivity() : r;
             if (handleTopActivityLaunchingInDifferentOrientation(
                     topCandidate, r, true /* checkOpening */)) {
                 // Display orientation should be deferred until the top fixed rotation is finished.
@@ -2101,13 +2101,13 @@
     }
 
     /**
-     * @see DisplayWindowPolicyController#canShowTasksInRecents()
+     * @see DisplayWindowPolicyController#canShowTasksInHostDeviceRecents()
      */
-    boolean canShowTasksInRecents() {
+    boolean canShowTasksInHostDeviceRecents() {
         if (mDwpcHelper == null) {
             return true;
         }
-        return mDwpcHelper.canShowTasksInRecents();
+        return mDwpcHelper.canShowTasksInHostDeviceRecents();
     }
 
     /**
@@ -2712,7 +2712,7 @@
         mWmService.mWindowsChanged = true;
         // If the transition finished callback cannot match the token for some reason, make sure the
         // rotated state is cleared if it is already invisible.
-        if (mFixedRotationLaunchingApp != null && !mFixedRotationLaunchingApp.mVisibleRequested
+        if (mFixedRotationLaunchingApp != null && !mFixedRotationLaunchingApp.isVisibleRequested()
                 && !mFixedRotationLaunchingApp.isVisible()
                 && !mDisplayRotation.isRotatingSeamlessly()) {
             clearFixedRotationLaunchingApp();
@@ -6814,10 +6814,9 @@
 
         @Override
         public boolean isRequestedVisible(@InsetsType int types) {
-            if (types == ime()) {
-                return getInsetsStateController().getImeSourceProvider().isImeShowing();
-            }
-            return (mRequestedVisibleTypes & types) != 0;
+            return ((types & ime()) != 0
+                            && getInsetsStateController().getImeSourceProvider().isImeShowing())
+                    || (mRequestedVisibleTypes & types) != 0;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 9ac1762..b419f36 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -57,7 +57,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
 import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
-import static android.view.WindowManager.TRANSIT_WAKE;
 import static android.view.WindowManagerGlobal.ADD_OKAY;
 import static android.view.WindowManagerPolicyConstants.ACTION_HDMI_PLUGGED;
 import static android.view.WindowManagerPolicyConstants.ALT_BAR_BOTTOM;
@@ -789,13 +788,7 @@
             if (!mDisplayContent.isDefaultDisplay) {
                 return;
             }
-            if (mAwake && mDisplayContent.mTransitionController.isShellTransitionsEnabled()
-                    && !mDisplayContent.mTransitionController.isCollecting()) {
-                // Start a transition for waking. This is needed for showWhenLocked activities.
-                mDisplayContent.mTransitionController.requestTransitionIfNeeded(TRANSIT_WAKE,
-                        0 /* flags */, null /* trigger */, mDisplayContent);
-            }
-            mService.mAtmService.mKeyguardController.updateDeferWakeTransition(
+            mService.mAtmService.mKeyguardController.updateDeferTransitionForAod(
                     mAwake /* waiting */);
         }
     }
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index eaa08fd..185e06e 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -1537,6 +1537,7 @@
         private int mHalfFoldSavedRotation = -1; // No saved rotation
         private DeviceStateController.FoldState mFoldState =
                 DeviceStateController.FoldState.UNKNOWN;
+        private boolean mInHalfFoldTransition = false;
 
         boolean overrideFrozenRotation() {
             return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
@@ -1544,6 +1545,7 @@
 
         boolean shouldRevertOverriddenRotation() {
             return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open.
+                    && mInHalfFoldTransition
                     && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted.
                     && mUserRotationMode
                     == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked.
@@ -1552,6 +1554,7 @@
         int revertOverriddenRotation() {
             int savedRotation = mHalfFoldSavedRotation;
             mHalfFoldSavedRotation = -1;
+            mInHalfFoldTransition = false;
             return savedRotation;
         }
 
@@ -1577,16 +1580,11 @@
                 mService.updateRotation(false /* alwaysSendConfiguration */,
                         false /* forceRelayout */);
             } else {
-                // Revert the rotation to our saved value if we transition from HALF_FOLDED.
-                if (mHalfFoldSavedRotation != -1) {
-                    mRotation = mHalfFoldSavedRotation;
-                }
-                // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here
-                // so we will override USER_ROTATION_LOCKED and allow a rotation).
+                mInHalfFoldTransition = true;
+                mFoldState = newState;
+                // Tell the device to update its orientation.
                 mService.updateRotation(false /* alwaysSendConfiguration */,
                         false /* forceRelayout */);
-                // Once we are rotated, set mFoldstate, effectively removing the lock override.
-                mFoldState = newState;
             }
         }
     }
@@ -1683,6 +1681,7 @@
 
     private static class RotationHistory {
         private static final int MAX_SIZE = 8;
+        private static final int NO_FOLD_CONTROLLER = -2;
         private static class Record {
             final @Surface.Rotation int mFromRotation;
             final @Surface.Rotation int mToRotation;
@@ -1694,6 +1693,9 @@
             final String mLastOrientationSource;
             final @ActivityInfo.ScreenOrientation int mSourceOrientation;
             final long mTimestamp = System.currentTimeMillis();
+            final int mHalfFoldSavedRotation;
+            final boolean mInHalfFoldTransition;
+            final DeviceStateController.FoldState mFoldState;
 
             Record(DisplayRotation dr, int fromRotation, int toRotation) {
                 mFromRotation = fromRotation;
@@ -1719,6 +1721,15 @@
                     mLastOrientationSource = null;
                     mSourceOrientation = SCREEN_ORIENTATION_UNSET;
                 }
+                if (dr.mFoldController != null) {
+                    mHalfFoldSavedRotation = dr.mFoldController.mHalfFoldSavedRotation;
+                    mInHalfFoldTransition = dr.mFoldController.mInHalfFoldTransition;
+                    mFoldState = dr.mFoldController.mFoldState;
+                } else {
+                    mHalfFoldSavedRotation = NO_FOLD_CONTROLLER;
+                    mInHalfFoldTransition = false;
+                    mFoldState = DeviceStateController.FoldState.UNKNOWN;
+                }
             }
 
             void dump(String prefix, PrintWriter pw) {
@@ -1735,6 +1746,12 @@
                 if (mNonDefaultRequestingTaskDisplayArea != null) {
                     pw.println(prefix + "  requestingTda=" + mNonDefaultRequestingTaskDisplayArea);
                 }
+                if (mHalfFoldSavedRotation != NO_FOLD_CONTROLLER) {
+                    pw.println(prefix + " halfFoldSavedRotation="
+                            + mHalfFoldSavedRotation
+                            + " mInHalfFoldTransition=" + mInHalfFoldTransition
+                            + " mFoldState=" + mFoldState);
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 6f821b5..69fd00c 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -153,13 +153,13 @@
     }
 
     /**
-     * @see DisplayWindowPolicyController#canShowTasksInRecents()
+     * @see DisplayWindowPolicyController#canShowTasksInHostDeviceRecents()
      */
-    public final boolean canShowTasksInRecents() {
+    public final boolean canShowTasksInHostDeviceRecents() {
         if (mDisplayWindowPolicyController == null) {
             return true;
         }
-        return mDisplayWindowPolicyController.canShowTasksInRecents();
+        return mDisplayWindowPolicyController.canShowTasksInHostDeviceRecents();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index 7bb036d..bd83794 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -191,7 +191,7 @@
             if (!r.attachedToProcess()) {
                 makeVisibleAndRestartIfNeeded(mStarting, mConfigChanges, isTop,
                         resumeTopActivity && isTop, r);
-            } else if (r.mVisibleRequested) {
+            } else if (r.isVisibleRequested()) {
                 // If this activity is already visible, then there is nothing to do here.
                 if (DEBUG_VISIBILITY) {
                     Slog.v(TAG_VISIBILITY, "Skipping: already visible at " + r);
@@ -244,7 +244,7 @@
         // invisible. If the app is already visible, it must have died while it was visible. In this
         // case, we'll show the dead window but will not restart the app. Otherwise we could end up
         // thrashing.
-        if (!isTop && r.mVisibleRequested && !r.isState(INITIALIZING)) {
+        if (!isTop && r.isVisibleRequested() && !r.isState(INITIALIZING)) {
             return;
         }
 
@@ -256,7 +256,7 @@
         if (r != starting) {
             r.startFreezingScreenLocked(configChanges);
         }
-        if (!r.mVisibleRequested || r.mLaunchTaskBehind) {
+        if (!r.isVisibleRequested() || r.mLaunchTaskBehind) {
             if (DEBUG_VISIBILITY) {
                 Slog.v(TAG_VISIBILITY, "Starting and making visible: " + r);
             }
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 1567fa7..e9badef 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -37,6 +37,7 @@
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_SHADE;
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER;
 
+import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
@@ -176,7 +177,7 @@
         final boolean keyguardChanged = (keyguardShowing != state.mKeyguardShowing)
                 || (state.mKeyguardGoingAway && keyguardShowing && !aodRemoved);
         if (aodRemoved) {
-            updateDeferWakeTransition(false /* waiting */);
+            updateDeferTransitionForAod(false /* waiting */);
         }
         if (!keyguardChanged && !aodChanged) {
             setWakeTransitionReady();
@@ -533,24 +534,25 @@
 
     private final Runnable mResetWaitTransition = () -> {
         synchronized (mWindowManager.mGlobalLock) {
-            updateDeferWakeTransition(false /* waiting */);
+            updateDeferTransitionForAod(false /* waiting */);
         }
     };
 
-    void updateDeferWakeTransition(boolean waiting) {
+    // Defer transition until AOD dismissed.
+    void updateDeferTransitionForAod(boolean waiting) {
         if (waiting == mWaitingForWakeTransition) {
             return;
         }
-        if (!mWindowManager.mAtmService.getTransitionController().isShellTransitionsEnabled()) {
+        if (!mService.getTransitionController().isCollecting()) {
             return;
         }
-        // if aod is showing, defer the wake transition until aod state changed.
+        // if AOD is showing, defer the wake transition until AOD state changed.
         if (waiting && isAodShowing(DEFAULT_DISPLAY)) {
             mWaitingForWakeTransition = true;
             mWindowManager.mAtmService.getTransitionController().deferTransitionReady();
             mWindowManager.mH.postDelayed(mResetWaitTransition, DEFER_WAKE_TRANSITION_TIMEOUT_MS);
         } else if (!waiting) {
-            // dismiss the deferring if the aod state change or cancel awake.
+            // dismiss the deferring if the AOD state change or cancel awake.
             mWaitingForWakeTransition = false;
             mWindowManager.mAtmService.getTransitionController().continueTransitionReady();
             mWindowManager.mH.removeCallbacks(mResetWaitTransition);
@@ -648,6 +650,12 @@
             mRequestDismissKeyguard = lastDismissKeyguardActivity != mDismissingKeyguardActivity
                     && !mOccluded && !mKeyguardGoingAway
                     && mDismissingKeyguardActivity != null;
+            if (mOccluded && mKeyguardShowing && !display.isSleeping() && !top.fillsParent()
+                    && display.mWallpaperController.getWallpaperTarget() == null) {
+                // The occluding activity may be translucent or not fill screen. Then let wallpaper
+                // to check whether it should set itself as target to avoid blank background.
+                display.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
+            }
 
             if (mTopTurnScreenOnActivity != lastTurnScreenOnActivity
                     && mTopTurnScreenOnActivity != null
@@ -657,10 +665,18 @@
                 mTopTurnScreenOnActivity.setCurrentLaunchCanTurnScreenOn(false);
             }
 
+            boolean hasChange = false;
             if (lastOccluded != mOccluded) {
                 controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
+                hasChange = true;
             } else if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
                 controller.handleKeyguardGoingAwayChanged(display);
+                hasChange = true;
+            }
+            // Collect the participates for shell transition, so that transition won't happen too
+            // early since the transition was set ready.
+            if (hasChange && top != null && (mOccluded || mKeyguardGoingAway)) {
+                display.mTransitionController.collect(top);
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index bb4c482..bcea6f4 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -673,6 +673,12 @@
                 + getHorizontalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
         pw.println(prefix + "  letterboxVerticalPositionMultiplier="
                 + getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
+        pw.println(prefix + "  letterboxPositionForHorizontalReachability="
+                + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
+                    mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+        pw.println(prefix + "  letterboxPositionForVerticalReachability="
+                + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
+                    mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
         pw.println(prefix + "  fixedOrientationLetterboxAspectRatio="
                 + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
         pw.println(prefix + "  defaultMinAspectRatioForUnresizableApps="
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 1fc061b..c827062 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -1393,7 +1393,7 @@
         // Ignore the task if it is started on a display which is not allow to show its tasks on
         // Recents.
         if (task.getDisplayContent() != null
-                && !task.getDisplayContent().canShowTasksInRecents()) {
+                && !task.getDisplayContent().canShowTasksInHostDeviceRecents()) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index ffe3374..be90588 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -112,7 +112,7 @@
                 mTargetActivityType);
         ActivityRecord targetActivity = getTargetActivity(targetRootTask);
         if (targetActivity != null) {
-            if (targetActivity.mVisibleRequested || targetActivity.isTopRunningActivity()) {
+            if (targetActivity.isVisibleRequested() || targetActivity.isTopRunningActivity()) {
                 // The activity is ready.
                 return;
             }
@@ -195,7 +195,7 @@
 
         // Send launch hint if we are actually launching the target. If it's already visible
         // (shouldn't happen in general) we don't need to send it.
-        if (targetActivity == null || !targetActivity.mVisibleRequested) {
+        if (targetActivity == null || !targetActivity.isVisibleRequested()) {
             mService.mRootWindowContainer.startPowerModeLaunchIfNeeded(
                     true /* forceSend */, targetActivity);
         }
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 64cca87..1ee4d6b 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2619,7 +2619,7 @@
         final ArrayList<Task> addedTasks = new ArrayList<>();
         forAllActivities((r) -> {
             final Task task = r.getTask();
-            if (r.mVisibleRequested && r.mStartingData == null && !addedTasks.contains(task)) {
+            if (r.isVisibleRequested() && r.mStartingData == null && !addedTasks.contains(task)) {
                 r.showStartingWindow(true /*taskSwitch*/);
                 addedTasks.add(task);
             }
@@ -2644,7 +2644,7 @@
         forAllLeafTasks(task -> {
             final int oldRank = task.mLayerRank;
             final ActivityRecord r = task.topRunningActivityLocked();
-            if (r != null && r.mVisibleRequested) {
+            if (r != null && r.isVisibleRequested()) {
                 task.mLayerRank = ++mTmpTaskLayerRank;
             } else {
                 task.mLayerRank = Task.LAYER_RANK_INVISIBLE;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 356cbda..a74a4b0 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -498,6 +498,12 @@
      */
     boolean mInRemoveTask;
 
+    /**
+     * When set, disassociate the leaf task if relaunched and reparented it to TDA as root task if
+     * possible.
+     */
+    boolean mReparentLeafTaskIfRelaunch;
+
     private final AnimatingActivityRegistry mAnimatingActivityRegistry =
             new AnimatingActivityRegistry();
 
@@ -2161,7 +2167,7 @@
     }
 
     private boolean shouldStartChangeTransition(int prevWinMode, @NonNull Rect prevBounds) {
-        if (!isLeafTask() || !canStartChangeTransition()) {
+        if (!(isLeafTask() || mCreatedByOrganizer) || !canStartChangeTransition()) {
             return false;
         }
         final int newWinMode = getWindowingMode();
@@ -2449,7 +2455,7 @@
 
         final String myReason = reason + " adjustFocusToNextFocusableTask";
         final ActivityRecord top = focusableTask.topRunningActivity();
-        if (focusableTask.isActivityTypeHome() && (top == null || !top.mVisibleRequested)) {
+        if (focusableTask.isActivityTypeHome() && (top == null || !top.isVisibleRequested())) {
             // If we will be focusing on the root home task next and its current top activity isn't
             // visible, then use the move the root home task to top to make the activity visible.
             focusableTask.getDisplayArea().moveHomeActivityToTop(myReason);
@@ -2762,7 +2768,7 @@
      */
     private static void getMaxVisibleBounds(ActivityRecord token, Rect out, boolean[] foundTop) {
         // skip hidden (or about to hide) apps
-        if (token.mIsExiting || !token.isClientVisible() || !token.mVisibleRequested) {
+        if (token.mIsExiting || !token.isClientVisible() || !token.isVisibleRequested()) {
             return;
         }
         final WindowState win = token.findMainWindow();
@@ -3071,7 +3077,7 @@
      * this activity.
      */
     ActivityRecord getTopVisibleActivity() {
-        return getActivity((r) -> !r.mIsExiting && r.isClientVisible() && r.mVisibleRequested);
+        return getActivity((r) -> !r.mIsExiting && r.isClientVisible() && r.isVisibleRequested());
     }
 
     /**
@@ -5720,7 +5726,7 @@
         forAllActivities(r -> {
             if (!r.info.packageName.equals(packageName)) return;
             r.forceNewConfig = true;
-            if (starting != null && r == starting && r.mVisibleRequested) {
+            if (starting != null && r == starting && r.isVisibleRequested()) {
                 r.startFreezingScreenLocked(CONFIG_SCREEN_LAYOUT);
             }
         });
@@ -6064,6 +6070,12 @@
         }
     }
 
+    void setReparentLeafTaskIfRelaunch(boolean reparentLeafTaskIfRelaunch) {
+        if (isOrganized()) {
+            mReparentLeafTaskIfRelaunch = reparentLeafTaskIfRelaunch;
+        }
+    }
+
     @Override
     public void dumpDebug(ProtoOutputStream proto, long fieldId,
             @WindowTraceLogLevel int logLevel) {
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index f3ed937..25b58fa 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -899,15 +899,16 @@
             }
         } else if (candidateTask != null) {
             final int position = onTop ? POSITION_TOP : POSITION_BOTTOM;
-            final Task launchRootTask = getLaunchRootTask(resolvedWindowingMode, activityType,
+            final Task launchParentTask = getLaunchRootTask(resolvedWindowingMode, activityType,
                     options, sourceTask, launchFlags, candidateTask);
-            if (launchRootTask != null) {
+            if (launchParentTask != null) {
                 if (candidateTask.getParent() == null) {
-                    launchRootTask.addChild(candidateTask, position);
-                } else if (candidateTask.getParent() != launchRootTask) {
-                    candidateTask.reparent(launchRootTask, position);
+                    launchParentTask.addChild(candidateTask, position);
+                } else if (candidateTask.getParent() != launchParentTask) {
+                    candidateTask.reparent(launchParentTask, position);
                 }
-            } else if (candidateTask.getDisplayArea() != this) {
+            } else if (candidateTask.getDisplayArea() != this
+                    || candidateTask.getRootTask().mReparentLeafTaskIfRelaunch) {
                 if (candidateTask.getParent() == null) {
                     addChild(candidateTask, position);
                 } else {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index d69d949..66b868a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -346,7 +346,7 @@
         }
 
         void process(ActivityRecord start, boolean preserveWindow) {
-            if (start == null || !start.mVisibleRequested) {
+            if (start == null || !start.isVisibleRequested()) {
                 return;
             }
             reset(preserveWindow);
@@ -1356,7 +1356,7 @@
         if (next.attachedToProcess()) {
             if (DEBUG_SWITCH) {
                 Slog.v(TAG_SWITCH, "Resume running: " + next + " stopped=" + next.stopped
-                        + " visibleRequested=" + next.mVisibleRequested);
+                        + " visibleRequested=" + next.isVisibleRequested());
             }
 
             // If the previous activity is translucent, force a visibility update of
@@ -1370,7 +1370,7 @@
                     || mLastPausedActivity != null && !mLastPausedActivity.occludesParent();
 
             // This activity is now becoming visible.
-            if (!next.mVisibleRequested || next.stopped || lastActivityTranslucent) {
+            if (!next.isVisibleRequested() || next.stopped || lastActivityTranslucent) {
                 next.app.addToPendingTop();
                 next.setVisibility(true);
             }
@@ -1421,7 +1421,7 @@
                     // Do over!
                     mTaskSupervisor.scheduleResumeTopActivities();
                 }
-                if (!next.mVisibleRequested || next.stopped) {
+                if (!next.isVisibleRequested() || next.stopped) {
                     next.setVisibility(true);
                 }
                 next.completeResumeLocked();
@@ -1732,7 +1732,7 @@
             } else if (prev.attachedToProcess()) {
                 ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s "
                                 + "wasStopping=%b visibleRequested=%b",  prev,  wasStopping,
-                        prev.mVisibleRequested);
+                        prev.isVisibleRequested());
                 if (prev.deferRelaunchUntilPaused) {
                     // Complete the deferred relaunch that was waiting for pause to complete.
                     ProtoLog.v(WM_DEBUG_STATES, "Re-launching after pause: %s", prev);
@@ -1742,7 +1742,7 @@
                     // We can't clobber it, because the stop confirmation will not be handled.
                     // We don't need to schedule another stop, we only need to let it happen.
                     prev.setState(STOPPING, "completePausedLocked");
-                } else if (!prev.mVisibleRequested || shouldSleepOrShutDownActivities()) {
+                } else if (!prev.isVisibleRequested() || shouldSleepOrShutDownActivities()) {
                     // Clear out any deferred client hide we might currently have.
                     prev.setDeferHidingClient(false);
                     // If we were visible then resumeTopActivities will release resources before
diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
index da73fad..ad46770 100644
--- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
@@ -165,9 +165,11 @@
         }
         // If the launch windowing mode is still undefined, inherit from the target task if the
         // task is already on the right display area (otherwise, the task may be on a different
-        // display area that has incompatible windowing mode).
+        // display area that has incompatible windowing mode or the task organizer request to
+        // disassociate the leaf task if relaunched and reparented it to TDA as root task).
         if (launchMode == WINDOWING_MODE_UNDEFINED
-                && task != null && task.getTaskDisplayArea() == suggestedDisplayArea) {
+                && task != null && task.getTaskDisplayArea() == suggestedDisplayArea
+                && !task.getRootTask().mReparentLeafTaskIfRelaunch) {
             launchMode = task.getWindowingMode();
             if (DEBUG) {
                 appendLog("inherit-from-task="
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 2b11d54..2737456 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -639,8 +639,8 @@
                 if (target.asDisplayContent() != null || target.asActivityRecord() != null) {
                     t.setCrop(targetLeash, null /* crop */);
                 } else {
-                    // Crop to the requested bounds.
-                    final Rect clipRect = target.getRequestedOverrideBounds();
+                    // Crop to the resolved override bounds.
+                    final Rect clipRect = target.getResolvedOverrideBounds();
                     t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height());
                 }
                 t.setCornerRadius(targetLeash, 0);
@@ -992,7 +992,7 @@
         // show here in the same way that we manually hide in finishTransaction.
         for (int i = mParticipants.size() - 1; i >= 0; --i) {
             final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
-            if (ar == null || !ar.mVisibleRequested) continue;
+            if (ar == null || !ar.isVisibleRequested()) continue;
             transaction.show(ar.getSurfaceControl());
 
             // Also manually show any non-reported parents. This is necessary in a few cases
@@ -1246,7 +1246,7 @@
         ArrayMap<WindowContainer, Integer> reasons = new ArrayMap<>();
         for (int i = mParticipants.size() - 1; i >= 0; --i) {
             ActivityRecord r = mParticipants.valueAt(i).asActivityRecord();
-            if (r == null || !r.mVisibleRequested) continue;
+            if (r == null || !r.isVisibleRequested()) continue;
             int transitionReason = APP_TRANSITION_WINDOWS_DRAWN;
             // At this point, r is "ready", but if it's not "ALL ready" then it is probably only
             // ready due to starting-window.
@@ -1617,8 +1617,11 @@
 
         WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, changes, topApp);
 
-        // make leash based on highest (z-order) direct child of ancestor with a participant.
-        WindowContainer leashReference = sortedTargets.get(0);
+        // Make leash based on highest (z-order) direct child of ancestor with a participant.
+        // TODO(b/261418859): Handle the case when the target contains window containers which
+        // belong to a different display. As a workaround we use topApp, from which wallpaper
+        // window container is removed, instead of sortedTargets here.
+        WindowContainer leashReference = topApp;
         while (leashReference.getParent() != ancestor) {
             leashReference = leashReference.getParent();
         }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 99527b1..cf541fc 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -38,6 +38,7 @@
 import android.util.ArrayMap;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
+import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.ITransitionMetricsReporter;
 import android.window.ITransitionPlayer;
@@ -116,6 +117,8 @@
      */
     boolean mBuildingFinishLayers = false;
 
+    private final SurfaceControl.Transaction mWakeT = new SurfaceControl.Transaction();
+
     TransitionController(ActivityTaskManagerService atm,
             TaskSnapshotController taskSnapshotController,
             TransitionTracer transitionTracer) {
@@ -619,8 +622,16 @@
     private void updateRunningRemoteAnimation(Transition transition, boolean isPlaying) {
         if (mTransitionPlayerProc == null) return;
         if (isPlaying) {
+            mWakeT.setEarlyWakeupStart();
+            mWakeT.apply();
+            // Usually transitions put quite a load onto the system already (with all the things
+            // happening in app), so pause task snapshot persisting to not increase the load.
+            mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(true);
             mTransitionPlayerProc.setRunningRemoteAnimation(true);
         } else if (mPlayingTransitions.isEmpty()) {
+            mWakeT.setEarlyWakeupEnd();
+            mWakeT.apply();
+            mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(false);
             mTransitionPlayerProc.setRunningRemoteAnimation(false);
             mRemotePlayer.clear();
             return;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 3b30dd1..5de143d9 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -725,9 +725,9 @@
         }
 
         final boolean newTargetHidden = wallpaperTarget.mActivityRecord != null
-                && !wallpaperTarget.mActivityRecord.mVisibleRequested;
+                && !wallpaperTarget.mActivityRecord.isVisibleRequested();
         final boolean oldTargetHidden = prevWallpaperTarget.mActivityRecord != null
-                && !prevWallpaperTarget.mActivityRecord.mVisibleRequested;
+                && !prevWallpaperTarget.mActivityRecord.isVisibleRequested();
 
         ProtoLog.v(WM_DEBUG_WALLPAPER, "Animating wallpapers: "
                 + "old: %s hidden=%b new: %s hidden=%b",
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 6ee30bb..32cfb70 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -136,7 +136,7 @@
                 recentsAnimationController.linkFixedRotationTransformIfNeeded(this);
             } else if ((wallpaperTarget.mActivityRecord == null
                     // Ignore invisible activity because it may be moving to background.
-                    || wallpaperTarget.mActivityRecord.mVisibleRequested)
+                    || wallpaperTarget.mActivityRecord.isVisibleRequested())
                     && wallpaperTarget.mToken.hasFixedRotationTransform()) {
                 // If the wallpaper target has a fixed rotation, we want the wallpaper to follow its
                 // rotation
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 2838304..46a30fb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1221,6 +1221,12 @@
             pw.println("Default position for vertical reachability: "
                     + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
                     mLetterboxConfiguration.getDefaultPositionForVerticalReachability()));
+            pw.println("Current position for horizontal reachability:"
+                    + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
+                        mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+            pw.println("Current position for vertical reachability:"
+                    + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
+                        mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
             pw.println("Is education enabled: "
                     + mLetterboxConfiguration.getIsEducationEnabled());
             pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index aa1cf56..738adc3 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -43,6 +43,7 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_SHORTCUT;
 
@@ -1233,7 +1234,7 @@
                 WindowContainer.fromBinder(hop.getContainer())
                         .removeLocalInsetsSourceProvider(hop.getInsetsTypes());
                 break;
-            case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP:
+            case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: {
                 final WindowContainer container = WindowContainer.fromBinder(hop.getContainer());
                 if (container == null || container.asDisplayArea() == null
                         || !container.isAttached()) {
@@ -1244,7 +1245,26 @@
                 container.setAlwaysOnTop(hop.isAlwaysOnTop());
                 effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 break;
-
+            }
+            case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH: {
+                final WindowContainer container = WindowContainer.fromBinder(hop.getContainer());
+                final Task task = container != null ? container.asTask() : null;
+                if (task == null || !task.isAttached()) {
+                    Slog.e(TAG, "Attempt to operate on unknown or detached container: "
+                            + container);
+                    break;
+                }
+                if (!task.mCreatedByOrganizer) {
+                    throw new UnsupportedOperationException(
+                            "Cannot set reparent leaf task flag on non-organized task : " + task);
+                }
+                if (!task.isRootTask()) {
+                    throw new UnsupportedOperationException(
+                            "Cannot set reparent leaf task flag on non-root task : " + task);
+                }
+                task.setReparentLeafTaskIfRelaunch(hop.isReparentLeafTaskIfRelaunch());
+                break;
+            }
         }
         return effects;
     }
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 8f63e93..d2cd8f8 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -765,7 +765,7 @@
         // - no longer visible OR
         // - not focusable (in PiP mode for instance)
         if (topDisplay == null
-                || !mPreQTopResumedActivity.mVisibleRequested
+                || !mPreQTopResumedActivity.isVisibleRequested()
                 || !mPreQTopResumedActivity.isFocusable()) {
             canUpdate = true;
         }
@@ -874,7 +874,7 @@
             // to those activities that are part of the package whose app-specific settings changed
             if (packageName.equals(r.packageName)
                     && r.applyAppSpecificConfig(nightMode, localesOverride)
-                    && r.mVisibleRequested) {
+                    && r.isVisibleRequested()) {
                 r.ensureActivityConfiguration(0 /* globalChanges */, true /* preserveWindow */);
             }
         }
@@ -956,7 +956,7 @@
             }
             // Don't consider any activities that are currently not in a state where they
             // can be destroyed.
-            if (r.mVisibleRequested || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
+            if (r.isVisibleRequested() || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
                     || r.isState(STARTED, RESUMED, PAUSING, PAUSED, STOPPING)) {
                 if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Not releasing in-use activity: " + r);
                 continue;
@@ -1002,7 +1002,7 @@
                 final int displayId = r.getDisplayId();
                 final Context c = root.getDisplayUiContext(displayId);
 
-                if (c != null && r.mVisibleRequested && !displayContexts.contains(c)) {
+                if (c != null && r.isVisibleRequested() && !displayContexts.contains(c)) {
                     displayContexts.add(c);
                 }
             }
@@ -1070,7 +1070,7 @@
             if (task != null && task.mLayerRank != Task.LAYER_RANK_INVISIBLE) {
                 stateFlags |= ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK;
             }
-            if (r.mVisibleRequested) {
+            if (r.isVisibleRequested()) {
                 if (r.isState(RESUMED)) {
                     stateFlags |= ACTIVITY_STATE_FLAG_HAS_RESUMED;
                 }
@@ -1282,7 +1282,7 @@
         }
         for (int i = activities.size() - 1; i >= 0; i--) {
             final ActivityRecord r = activities.get(i);
-            if (r.mVisibleRequested || r.isVisible()) {
+            if (r.isVisibleRequested() || r.isVisible()) {
                 // While an activity launches a new activity, it's possible that the old activity
                 // is already requested to be hidden (mVisibleRequested=false), but this visibility
                 // is not yet committed, so isVisible()=true.
@@ -1503,7 +1503,7 @@
             Configuration overrideConfig = new Configuration(r.getRequestedOverrideConfiguration());
             overrideConfig.assetsSeq = assetSeq;
             r.onRequestedOverrideConfigurationChanged(overrideConfig);
-            if (r.mVisibleRequested) {
+            if (r.isVisibleRequested()) {
                 r.ensureActivityConfiguration(0, true);
             }
         }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 73759d3..c6578ef 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1957,7 +1957,7 @@
      */
     // TODO: Can we consolidate this with #isVisible() or have a more appropriate name for this?
     boolean isWinVisibleLw() {
-        return (mActivityRecord == null || mActivityRecord.mVisibleRequested
+        return (mActivityRecord == null || mActivityRecord.isVisibleRequested()
                 || mActivityRecord.isAnimating(TRANSITION | PARENTS)) && isVisible();
     }
 
@@ -1994,7 +1994,7 @@
         final ActivityRecord atoken = mActivityRecord;
         return (mHasSurface || (!mRelayoutCalled && mViewVisibility == View.VISIBLE))
                 && isVisibleByPolicy() && !isParentWindowHidden()
-                && (atoken == null || atoken.mVisibleRequested)
+                && (atoken == null || atoken.isVisibleRequested())
                 && !mAnimatingExit && !mDestroying;
     }
 
@@ -2101,7 +2101,7 @@
     boolean isDisplayed() {
         final ActivityRecord atoken = mActivityRecord;
         return isDrawn() && isVisibleByPolicy()
-                && ((!isParentWindowHidden() && (atoken == null || atoken.mVisibleRequested))
+                && ((!isParentWindowHidden() && (atoken == null || atoken.isVisibleRequested()))
                         || isAnimating(TRANSITION | PARENTS));
     }
 
@@ -2123,7 +2123,7 @@
                 // a layout since they can request relayout when client visibility is false.
                 // TODO (b/157682066) investigate if we can clean up isVisible
                 || (atoken == null && !(wouldBeVisibleIfPolicyIgnored() && isVisibleByPolicy()))
-                || (atoken != null && !atoken.mVisibleRequested)
+                || (atoken != null && !atoken.isVisibleRequested())
                 || isParentWindowGoneForLayout()
                 || (mAnimatingExit && !isAnimatingLw())
                 || mDestroying;
@@ -2170,7 +2170,7 @@
             return;
         }
         if (mActivityRecord != null) {
-            if (!mActivityRecord.mVisibleRequested) return;
+            if (!mActivityRecord.isVisibleRequested()) return;
             if (mActivityRecord.allDrawn) {
                 // The allDrawn of activity is reset when the visibility is changed to visible, so
                 // the content should be ready if allDrawn is set.
@@ -2743,7 +2743,7 @@
                         + " exiting=" + mAnimatingExit + " destroying=" + mDestroying);
                 if (mActivityRecord != null) {
                     Slog.i(TAG_WM, "  mActivityRecord.visibleRequested="
-                            + mActivityRecord.mVisibleRequested);
+                            + mActivityRecord.isVisibleRequested());
                 }
             }
         }
@@ -3219,7 +3219,7 @@
         }
 
         return !mActivityRecord.getTask().getRootTask().shouldIgnoreInput()
-                && mActivityRecord.mVisibleRequested;
+                && mActivityRecord.isVisibleRequested();
     }
 
     /**
@@ -3875,7 +3875,7 @@
         // the client erroneously accepting a configuration that would have otherwise caused an
         // activity restart. We instead hand back the last reported {@link MergedConfiguration}.
         if (useLatestConfig || (relayoutVisible && (mActivityRecord == null
-                || mActivityRecord.mVisibleRequested))) {
+                || mActivityRecord.isVisibleRequested()))) {
             final Configuration globalConfig = getProcessGlobalConfiguration();
             final Configuration overrideConfig = getMergedOverrideConfiguration();
             outMergedConfiguration.setConfiguration(globalConfig, overrideConfig);
@@ -4734,7 +4734,7 @@
                     + " during animation: policyVis=" + isVisibleByPolicy()
                     + " parentHidden=" + isParentWindowHidden()
                     + " tok.visibleRequested="
-                    + (mActivityRecord != null && mActivityRecord.mVisibleRequested)
+                    + (mActivityRecord != null && mActivityRecord.isVisibleRequested())
                     + " tok.visible=" + (mActivityRecord != null && mActivityRecord.isVisible())
                     + " animating=" + isAnimating(TRANSITION | PARENTS)
                     + " tok animating="
@@ -5195,7 +5195,7 @@
                         + " pv=" + isVisibleByPolicy()
                         + " mDrawState=" + mWinAnimator.mDrawState
                         + " ph=" + isParentWindowHidden()
-                        + " th=" + (mActivityRecord != null && mActivityRecord.mVisibleRequested)
+                        + " th=" + (mActivityRecord != null && mActivityRecord.isVisibleRequested())
                         + " a=" + isAnimating(TRANSITION | PARENTS));
             }
         }
diff --git a/services/core/java/com/android/server/wm/utils/StateMachine.java b/services/core/java/com/android/server/wm/utils/StateMachine.java
new file mode 100644
index 0000000..91a5dc4
--- /dev/null
+++ b/services/core/java/com/android/server/wm/utils/StateMachine.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.wm.utils;
+
+import android.annotation.IntRange;
+import android.annotation.Nullable;
+import android.util.IntArray;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.util.AnnotationValidations;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * Simple hierarchical state machine.
+ *
+ * The state is represented by an integer value. The root state has a value {@code 0x0}, and top
+ * level state has a value in range {@code 0x1} to {@code 0xF}. To indicate a state B is a sub state
+ * of a state A, assign an integer state_value(B) = state_value(A) << 4 + (0x0 .. 0xF).
+ */
+public class StateMachine {
+    private static final String TAG = "StateMachine";
+
+    /**
+     * Interface for implementing state specific actions.
+     */
+    public interface Handler {
+        /**
+         * Called when state machine changes its state to this state.
+         */
+        default void enter() {}
+
+        /**
+         * Called when state machine changes its state from this state to other state.
+         */
+        default void exit() {}
+
+        /**
+         * @param event type of this event.
+         * @param param parameter passed to {@link StateMachine#handle(int, Object)}
+         * @return {@code true} if the event was handled in this handler, so we don't need to
+         *          check the parent state. Otherwise, handle() of the parent state is triggered.
+         */
+        default boolean handle(int event, @Nullable Object param) {
+            return false;
+        }
+    }
+
+    /**
+     * The most recent state requested by transit() call.
+     *
+     * @note When transit() is called recursively, this might not be same value as mState until
+     *       transit() finishes.
+     */
+    private int mLastRequestedState;
+
+    /**
+     * The current state of this state machine.
+     */
+    private int mState;
+
+    private final IntArray mTmp = new IntArray();
+    private final SparseArray<Handler> mStateHandlers = new SparseArray<>();
+
+    /**
+     * Actions which need to execute to finish requested transition.
+     */
+    private final Queue<Command> mCommands = new ArrayDeque<>();
+
+    protected static class Command {
+        static final int COMMIT = 1;
+        static final int ENTER = 2;
+        static final int EXIT = 3;
+
+        final int mType;
+        final int mState;
+
+        private Command(int type, @IntRange(from = 0) int state) {
+            mType = type;
+            AnnotationValidations.validate(IntRange.class, null, state, "from", 0);
+            mState = state;
+        }
+
+        static Command newCommit(int state) {
+            return new Command(COMMIT, state);
+        }
+
+        static Command newEnter(int state) {
+            return new Command(ENTER, state);
+        }
+
+        static Command newExit(int state) {
+            return new Command(EXIT, state);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Command{ type: ");
+            switch (mType) {
+                case COMMIT:
+                    sb.append("commit");
+                    break;
+                case ENTER:
+                    sb.append("enter");
+                    break;
+                case EXIT:
+                    sb.append("exit");
+                    break;
+                default:
+                    sb.append("UNKNOWN(");
+                    sb.append(mType);
+                    sb.append(")");
+                    break;
+            }
+            sb.append(" state: ");
+            sb.append(Integer.toHexString(mState));
+            sb.append(" }");
+            return sb.toString();
+        }
+    }
+
+    public StateMachine() {
+        this(0);
+    }
+
+    public StateMachine(@IntRange(from = 0) int initialState) {
+        mState = initialState;
+        AnnotationValidations.validate(IntRange.class, null, initialState, "from", 0);
+        mLastRequestedState = initialState;
+    }
+
+    /**
+     * @see #mLastRequestedState
+     */
+    public int getState() {
+        return mLastRequestedState;
+    }
+
+    protected int getCurrentState() {
+        return mState;
+    }
+
+    protected Command[] getCommands() {
+        final Command[] commands = new Command[mCommands.size()];
+        mCommands.toArray(commands);
+        return commands;
+    }
+
+    /**
+     * Add a handler for a specific state.
+     *
+     * @param state State which the given handler processes.
+     * @param handler A handler which runs entry, exit actions and processes events.
+     * @return Previous state handler if it's already registered, or {@code null}.
+     */
+    @Nullable public Handler addStateHandler(int state, @Nullable Handler handler) {
+        final Handler handlerOld = mStateHandlers.get(state);
+        mStateHandlers.put(state, handler);
+        return handlerOld;
+    }
+
+    /**
+     * Process an event. Search handler for a given event and {@link Handler#handle(int)}. If the
+     * handler cannot handle the event, delegate it to a handler for a parent of the given state.
+     *
+     * @param event Type of an event.
+     */
+    public void handle(int event, @Nullable Object param) {
+        int state = mState;
+        while (state != 0) {
+            final Handler h = mStateHandlers.get(state);
+            if (h != null && h.handle(event, param)) {
+                return;
+            }
+            state >>= 4;
+        }
+    }
+
+    protected void enter(@IntRange(from = 0) int state) {
+        AnnotationValidations.validate(IntRange.class, null, state, "from", 0);
+        final Handler h = mStateHandlers.get(state);
+        if (h != null) {
+            h.enter();
+        }
+    }
+
+    protected void exit(@IntRange(from = 0) int state) {
+        AnnotationValidations.validate(IntRange.class, null, state, "from", 0);
+        final Handler h = mStateHandlers.get(state);
+        if (h != null) {
+            h.exit();
+        }
+    }
+
+    /**
+     * @return {@code true} if a given sub state is a descendant of a given super state.
+     */
+    public static boolean isIn(int subState, int superState) {
+        while (subState > superState) {
+            subState >>= 4;
+        }
+        return subState == superState;
+    }
+
+    /**
+     * Check if the last requested state is a sub state of a given state.
+     *
+     * @return {@code true} if the last requested state (via {@link #transit(int)}) is a sub state
+     *         of a given state.
+     */
+    public boolean isIn(int state) {
+        return isIn(mLastRequestedState, state);
+    }
+
+    /**
+     * Change state to the requested state.
+     *
+     * @param newState The new state that the state machine should be changed.
+     */
+    public void transit(@IntRange(from = 0) int newState) {
+        AnnotationValidations.validate(IntRange.class, null, newState, "from", 0);
+
+        // entry and exit action might start another transition, so this transit() function can be
+        // called recursively. In order to guarantee entry and exit actions in expected order,
+        // we first compute the sequence and push them into a queue, then process them later.
+        mCommands.add(Command.newCommit(newState));
+        if (mLastRequestedState == newState) {
+            mCommands.add(Command.newExit(newState));
+            mCommands.add(Command.newEnter(newState));
+        } else {
+            // mLastRequestedState to least common ancestor
+            for (int s = mLastRequestedState; !isIn(newState, s); s >>= 4) {
+                mCommands.add(Command.newExit(s));
+            }
+            // least common ancestor to newState
+            mTmp.clear();
+            for (int s = newState; !isIn(mLastRequestedState, s); s >>= 4) {
+                mTmp.add(s);
+            }
+            for (int i = mTmp.size() - 1; i >= 0; --i) {
+                mCommands.add(Command.newEnter(mTmp.get(i)));
+            }
+        }
+        mLastRequestedState = newState;
+        while (!mCommands.isEmpty()) {
+            final Command cmd = mCommands.remove();
+            switch (cmd.mType) {
+                case Command.EXIT:
+                    exit(cmd.mState);
+                    break;
+                case Command.ENTER:
+                    enter(cmd.mState);
+                    break;
+                case Command.COMMIT:
+                    mState = cmd.mState;
+                    break;
+                default:
+                    Slog.e(TAG, "Unknown command type: " + cmd.mType);
+                    break;
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/utils/TEST_MAPPING b/services/core/java/com/android/server/wm/utils/TEST_MAPPING
new file mode 100644
index 0000000..aa69d2a
--- /dev/null
+++ b/services/core/java/com/android/server/wm/utils/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+  "presubmit": [
+    {
+      "name": "WmTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.wm.utils"
+        },
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index f431250..e661688 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -31,6 +31,9 @@
         "BroadcastRadio/convert.cpp",
         "BroadcastRadio/regions.cpp",
         "stats/SurfaceFlingerPuller.cpp",
+        "tvinput/BufferProducerThread.cpp",
+        "tvinput/JTvInputHal.cpp",
+        "tvinput/TvInputHal_hidl.cpp",
         "com_android_server_adb_AdbDebuggingManager.cpp",
         "com_android_server_am_BatteryStatsService.cpp",
         "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp",
@@ -150,6 +153,7 @@
         "libdataloader",
         "libincfs",
         "android.hardware.audio.common@2.0",
+        "android.media.audio.common.types-V1-ndk",
         "android.hardware.broadcastradio@1.0",
         "android.hardware.broadcastradio@1.1",
         "android.hardware.contexthub@1.0",
@@ -176,6 +180,7 @@
         "android.hardware.power.stats-V1-ndk",
         "android.hardware.thermal@1.0",
         "android.hardware.tv.input@1.0",
+        "android.hardware.tv.input-V1-ndk",
         "android.hardware.vibrator-V2-cpp",
         "android.hardware.vibrator@1.0",
         "android.hardware.vibrator@1.1",
@@ -197,6 +202,7 @@
 
     static_libs: [
         "android.hardware.broadcastradio@common-utils-1x-lib",
+        "libaidlcommonsupport",
     ],
 
     product_variables: {
diff --git a/services/core/jni/com_android_server_tv_TvInputHal.cpp b/services/core/jni/com_android_server_tv_TvInputHal.cpp
index a5311f3..a8d2f4e 100644
--- a/services/core/jni/com_android_server_tv_TvInputHal.cpp
+++ b/services/core/jni/com_android_server_tv_TvInputHal.cpp
@@ -18,577 +18,15 @@
 
 //#define LOG_NDEBUG 0
 
-#include "android_os_MessageQueue.h"
-#include "android_runtime/AndroidRuntime.h"
-#include "android_runtime/android_view_Surface.h"
-#include <nativehelper/JNIHelp.h>
-#include "jni.h"
+#include "tvinput/JTvInputHal.h"
 
-#include <android/hardware/tv/input/1.0/ITvInputCallback.h>
-#include <android/hardware/tv/input/1.0/ITvInput.h>
-#include <android/hardware/tv/input/1.0/types.h>
-#include <gui/Surface.h>
-#include <utils/Errors.h>
-#include <utils/KeyedVector.h>
-#include <utils/Log.h>
-#include <utils/Looper.h>
-#include <utils/NativeHandle.h>
-#include <hardware/tv_input.h>
-
-using ::android::hardware::audio::common::V2_0::AudioDevice;
-using ::android::hardware::tv::input::V1_0::ITvInput;
-using ::android::hardware::tv::input::V1_0::ITvInputCallback;
-using ::android::hardware::tv::input::V1_0::Result;
-using ::android::hardware::tv::input::V1_0::TvInputDeviceInfo;
-using ::android::hardware::tv::input::V1_0::TvInputEvent;
-using ::android::hardware::tv::input::V1_0::TvInputEventType;
-using ::android::hardware::tv::input::V1_0::TvInputType;
-using ::android::hardware::tv::input::V1_0::TvStreamConfig;
-using ::android::hardware::Return;
-using ::android::hardware::Void;
-using ::android::hardware::hidl_vec;
-using ::android::hardware::hidl_string;
+gTvInputHalClassInfoType gTvInputHalClassInfo;
+gTvStreamConfigClassInfoType gTvStreamConfigClassInfo;
+gTvStreamConfigBuilderClassInfoType gTvStreamConfigBuilderClassInfo;
+gTvInputHardwareInfoBuilderClassInfoType gTvInputHardwareInfoBuilderClassInfo;
 
 namespace android {
 
-static struct {
-    jmethodID deviceAvailable;
-    jmethodID deviceUnavailable;
-    jmethodID streamConfigsChanged;
-    jmethodID firstFrameCaptured;
-} gTvInputHalClassInfo;
-
-static struct {
-    jclass clazz;
-} gTvStreamConfigClassInfo;
-
-static struct {
-    jclass clazz;
-
-    jmethodID constructor;
-    jmethodID streamId;
-    jmethodID type;
-    jmethodID maxWidth;
-    jmethodID maxHeight;
-    jmethodID generation;
-    jmethodID build;
-} gTvStreamConfigBuilderClassInfo;
-
-static struct {
-    jclass clazz;
-
-    jmethodID constructor;
-    jmethodID deviceId;
-    jmethodID type;
-    jmethodID hdmiPortId;
-    jmethodID cableConnectionStatus;
-    jmethodID audioType;
-    jmethodID audioAddress;
-    jmethodID build;
-} gTvInputHardwareInfoBuilderClassInfo;
-
-////////////////////////////////////////////////////////////////////////////////
-
-class BufferProducerThread : public Thread {
-public:
-    BufferProducerThread(tv_input_device_t* device, int deviceId, const tv_stream_t* stream);
-
-    virtual status_t readyToRun();
-
-    void setSurface(const sp<Surface>& surface);
-    void onCaptured(uint32_t seq, bool succeeded);
-    void shutdown();
-
-private:
-    Mutex mLock;
-    Condition mCondition;
-    sp<Surface> mSurface;
-    tv_input_device_t* mDevice;
-    int mDeviceId;
-    tv_stream_t mStream;
-    sp<ANativeWindowBuffer_t> mBuffer;
-    enum {
-        CAPTURING,
-        CAPTURED,
-        RELEASED,
-    } mBufferState;
-    uint32_t mSeq;
-    bool mShutdown;
-
-    virtual bool threadLoop();
-
-    void setSurfaceLocked(const sp<Surface>& surface);
-};
-
-BufferProducerThread::BufferProducerThread(
-        tv_input_device_t* device, int deviceId, const tv_stream_t* stream)
-    : Thread(false),
-      mDevice(device),
-      mDeviceId(deviceId),
-      mBuffer(NULL),
-      mBufferState(RELEASED),
-      mSeq(0u),
-      mShutdown(false) {
-    memcpy(&mStream, stream, sizeof(mStream));
-}
-
-status_t BufferProducerThread::readyToRun() {
-    sp<ANativeWindow> anw(mSurface);
-    status_t err = native_window_set_usage(anw.get(), mStream.buffer_producer.usage);
-    if (err != NO_ERROR) {
-        return err;
-    }
-    err = native_window_set_buffers_dimensions(
-            anw.get(), mStream.buffer_producer.width, mStream.buffer_producer.height);
-    if (err != NO_ERROR) {
-        return err;
-    }
-    err = native_window_set_buffers_format(anw.get(), mStream.buffer_producer.format);
-    if (err != NO_ERROR) {
-        return err;
-    }
-    return NO_ERROR;
-}
-
-void BufferProducerThread::setSurface(const sp<Surface>& surface) {
-    Mutex::Autolock autoLock(&mLock);
-    setSurfaceLocked(surface);
-}
-
-void BufferProducerThread::setSurfaceLocked(const sp<Surface>& surface) {
-    if (surface == mSurface) {
-        return;
-    }
-
-    if (mBufferState == CAPTURING) {
-        mDevice->cancel_capture(mDevice, mDeviceId, mStream.stream_id, mSeq);
-    }
-    while (mBufferState == CAPTURING) {
-        status_t err = mCondition.waitRelative(mLock, s2ns(1));
-        if (err != NO_ERROR) {
-            ALOGE("error %d while wating for buffer state to change.", err);
-            break;
-        }
-    }
-    mBuffer.clear();
-    mBufferState = RELEASED;
-
-    mSurface = surface;
-    mCondition.broadcast();
-}
-
-void BufferProducerThread::onCaptured(uint32_t seq, bool succeeded) {
-    Mutex::Autolock autoLock(&mLock);
-    if (seq != mSeq) {
-        ALOGW("Incorrect sequence value: expected %u actual %u", mSeq, seq);
-    }
-    if (mBufferState != CAPTURING) {
-        ALOGW("mBufferState != CAPTURING : instead %d", mBufferState);
-    }
-    if (succeeded) {
-        mBufferState = CAPTURED;
-    } else {
-        mBuffer.clear();
-        mBufferState = RELEASED;
-    }
-    mCondition.broadcast();
-}
-
-void BufferProducerThread::shutdown() {
-    Mutex::Autolock autoLock(&mLock);
-    mShutdown = true;
-    setSurfaceLocked(NULL);
-    requestExitAndWait();
-}
-
-bool BufferProducerThread::threadLoop() {
-    Mutex::Autolock autoLock(&mLock);
-
-    status_t err = NO_ERROR;
-    if (mSurface == NULL) {
-        err = mCondition.waitRelative(mLock, s2ns(1));
-        // It's OK to time out here.
-        if (err != NO_ERROR && err != TIMED_OUT) {
-            ALOGE("error %d while wating for non-null surface to be set", err);
-            return false;
-        }
-        return true;
-    }
-    sp<ANativeWindow> anw(mSurface);
-    while (mBufferState == CAPTURING) {
-        err = mCondition.waitRelative(mLock, s2ns(1));
-        if (err != NO_ERROR) {
-            ALOGE("error %d while wating for buffer state to change.", err);
-            return false;
-        }
-    }
-    if (mBufferState == CAPTURED && anw != NULL) {
-        err = anw->queueBuffer(anw.get(), mBuffer.get(), -1);
-        if (err != NO_ERROR) {
-            ALOGE("error %d while queueing buffer to surface", err);
-            return false;
-        }
-        mBuffer.clear();
-        mBufferState = RELEASED;
-    }
-    if (mBuffer == NULL && !mShutdown && anw != NULL) {
-        ANativeWindowBuffer_t* buffer = NULL;
-        err = native_window_dequeue_buffer_and_wait(anw.get(), &buffer);
-        if (err != NO_ERROR) {
-            ALOGE("error %d while dequeueing buffer to surface", err);
-            return false;
-        }
-        mBuffer = buffer;
-        mBufferState = CAPTURING;
-        mDevice->request_capture(mDevice, mDeviceId, mStream.stream_id,
-                                 buffer->handle, ++mSeq);
-    }
-
-    return true;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
-class JTvInputHal {
-public:
-    ~JTvInputHal();
-
-    static JTvInputHal* createInstance(JNIEnv* env, jobject thiz, const sp<Looper>& looper);
-
-    int addOrUpdateStream(int deviceId, int streamId, const sp<Surface>& surface);
-    int removeStream(int deviceId, int streamId);
-    const hidl_vec<TvStreamConfig> getStreamConfigs(int deviceId);
-
-    void onDeviceAvailable(const TvInputDeviceInfo& info);
-    void onDeviceUnavailable(int deviceId);
-    void onStreamConfigurationsChanged(int deviceId, int cableConnectionStatus);
-    void onCaptured(int deviceId, int streamId, uint32_t seq, bool succeeded);
-
-private:
-    // Connection between a surface and a stream.
-    class Connection {
-    public:
-        Connection() {}
-
-        sp<Surface> mSurface;
-        tv_stream_type_t mStreamType;
-
-        // Only valid when mStreamType == TV_STREAM_TYPE_INDEPENDENT_VIDEO_SOURCE
-        sp<NativeHandle> mSourceHandle;
-        // Only valid when mStreamType == TV_STREAM_TYPE_BUFFER_PRODUCER
-        sp<BufferProducerThread> mThread;
-    };
-
-    class NotifyHandler : public MessageHandler {
-    public:
-        NotifyHandler(JTvInputHal* hal, const TvInputEvent& event);
-
-        virtual void handleMessage(const Message& message);
-
-    private:
-        TvInputEvent mEvent;
-        JTvInputHal* mHal;
-    };
-
-    class TvInputCallback : public ITvInputCallback {
-    public:
-        explicit TvInputCallback(JTvInputHal* hal);
-        Return<void> notify(const TvInputEvent& event) override;
-    private:
-        JTvInputHal* mHal;
-    };
-
-    JTvInputHal(JNIEnv* env, jobject thiz, sp<ITvInput> tvInput, const sp<Looper>& looper);
-
-    Mutex mLock;
-    Mutex mStreamLock;
-    jweak mThiz;
-    sp<Looper> mLooper;
-
-    KeyedVector<int, KeyedVector<int, Connection> > mConnections;
-
-    sp<ITvInput> mTvInput;
-    sp<ITvInputCallback> mTvInputCallback;
-};
-
-JTvInputHal::JTvInputHal(JNIEnv* env, jobject thiz, sp<ITvInput> tvInput,
-        const sp<Looper>& looper) {
-    mThiz = env->NewWeakGlobalRef(thiz);
-    mTvInput = tvInput;
-    mLooper = looper;
-    mTvInputCallback = new TvInputCallback(this);
-    mTvInput->setCallback(mTvInputCallback);
-}
-
-JTvInputHal::~JTvInputHal() {
-    mTvInput->setCallback(nullptr);
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->DeleteWeakGlobalRef(mThiz);
-    mThiz = NULL;
-}
-
-JTvInputHal* JTvInputHal::createInstance(JNIEnv* env, jobject thiz, const sp<Looper>& looper) {
-    // TODO(b/31632518)
-    sp<ITvInput> tvInput = ITvInput::getService();
-    if (tvInput == nullptr) {
-        ALOGE("Couldn't get tv.input service.");
-        return nullptr;
-    }
-
-    return new JTvInputHal(env, thiz, tvInput, looper);
-}
-
-int JTvInputHal::addOrUpdateStream(int deviceId, int streamId, const sp<Surface>& surface) {
-    Mutex::Autolock autoLock(&mStreamLock);
-    KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
-    if (connections.indexOfKey(streamId) < 0) {
-        connections.add(streamId, Connection());
-    }
-    Connection& connection = connections.editValueFor(streamId);
-    if (connection.mSurface == surface) {
-        // Nothing to do
-        return NO_ERROR;
-    }
-    // Clear the surface in the connection.
-    if (connection.mSurface != NULL) {
-        if (connection.mStreamType == TV_STREAM_TYPE_INDEPENDENT_VIDEO_SOURCE) {
-            if (Surface::isValid(connection.mSurface)) {
-                connection.mSurface->setSidebandStream(NULL);
-            }
-        }
-        connection.mSurface.clear();
-    }
-    if (connection.mSourceHandle == NULL && connection.mThread == NULL) {
-        // Need to configure stream
-        Result result = Result::UNKNOWN;
-        hidl_vec<TvStreamConfig> list;
-        mTvInput->getStreamConfigurations(deviceId,
-                [&result, &list](Result res, hidl_vec<TvStreamConfig> configs) {
-                    result = res;
-                    if (res == Result::OK) {
-                        list = configs;
-                    }
-                });
-        if (result != Result::OK) {
-            ALOGE("Couldn't get stream configs for device id:%d result:%d", deviceId, result);
-            return UNKNOWN_ERROR;
-        }
-        int configIndex = -1;
-        for (size_t i = 0; i < list.size(); ++i) {
-            if (list[i].streamId == streamId) {
-                configIndex = i;
-                break;
-            }
-        }
-        if (configIndex == -1) {
-            ALOGE("Cannot find a config with given stream ID: %d", streamId);
-            return BAD_VALUE;
-        }
-        connection.mStreamType = TV_STREAM_TYPE_INDEPENDENT_VIDEO_SOURCE;
-
-        result = Result::UNKNOWN;
-        const native_handle_t* sidebandStream;
-        mTvInput->openStream(deviceId, streamId,
-                [&result, &sidebandStream](Result res, const native_handle_t* handle) {
-                    result = res;
-                    if (res == Result::OK) {
-                        if (handle) {
-                            sidebandStream = native_handle_clone(handle);
-                        } else {
-                            result = Result::UNKNOWN;
-                        }
-                    }
-                });
-        if (result != Result::OK) {
-            ALOGE("Couldn't open stream. device id:%d stream id:%d result:%d", deviceId, streamId,
-                    result);
-            return UNKNOWN_ERROR;
-        }
-        connection.mSourceHandle = NativeHandle::create((native_handle_t*)sidebandStream, true);
-    }
-    connection.mSurface = surface;
-    if (connection.mSurface != nullptr) {
-        connection.mSurface->setSidebandStream(connection.mSourceHandle);
-    }
-    return NO_ERROR;
-}
-
-int JTvInputHal::removeStream(int deviceId, int streamId) {
-    Mutex::Autolock autoLock(&mStreamLock);
-    KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
-    if (connections.indexOfKey(streamId) < 0) {
-        return BAD_VALUE;
-    }
-    Connection& connection = connections.editValueFor(streamId);
-    if (connection.mSurface == NULL) {
-        // Nothing to do
-        return NO_ERROR;
-    }
-    if (Surface::isValid(connection.mSurface)) {
-        connection.mSurface->setSidebandStream(NULL);
-    }
-    connection.mSurface.clear();
-    if (connection.mThread != NULL) {
-        connection.mThread->shutdown();
-        connection.mThread.clear();
-    }
-    if (mTvInput->closeStream(deviceId, streamId) != Result::OK) {
-        ALOGE("Couldn't close stream. device id:%d stream id:%d", deviceId, streamId);
-        return BAD_VALUE;
-    }
-    if (connection.mSourceHandle != NULL) {
-        connection.mSourceHandle.clear();
-    }
-    return NO_ERROR;
-}
-
-const hidl_vec<TvStreamConfig> JTvInputHal::getStreamConfigs(int deviceId) {
-    Result result = Result::UNKNOWN;
-    hidl_vec<TvStreamConfig> list;
-    mTvInput->getStreamConfigurations(deviceId,
-            [&result, &list](Result res, hidl_vec<TvStreamConfig> configs) {
-                result = res;
-                if (res == Result::OK) {
-                    list = configs;
-                }
-            });
-    if (result != Result::OK) {
-        ALOGE("Couldn't get stream configs for device id:%d result:%d", deviceId, result);
-    }
-    return list;
-}
-
-void JTvInputHal::onDeviceAvailable(const TvInputDeviceInfo& info) {
-    {
-        Mutex::Autolock autoLock(&mLock);
-        mConnections.add(info.deviceId, KeyedVector<int, Connection>());
-    }
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-
-    jobject builder = env->NewObject(
-            gTvInputHardwareInfoBuilderClassInfo.clazz,
-            gTvInputHardwareInfoBuilderClassInfo.constructor);
-    env->CallObjectMethod(
-            builder, gTvInputHardwareInfoBuilderClassInfo.deviceId, info.deviceId);
-    env->CallObjectMethod(
-            builder, gTvInputHardwareInfoBuilderClassInfo.type, info.type);
-    if (info.type == TvInputType::HDMI) {
-        env->CallObjectMethod(
-                builder, gTvInputHardwareInfoBuilderClassInfo.hdmiPortId, info.portId);
-    }
-    env->CallObjectMethod(
-            builder, gTvInputHardwareInfoBuilderClassInfo.cableConnectionStatus,
-            info.cableConnectionStatus);
-    env->CallObjectMethod(
-            builder, gTvInputHardwareInfoBuilderClassInfo.audioType, info.audioType);
-    if (info.audioType != AudioDevice::NONE) {
-        uint8_t buffer[info.audioAddress.size() + 1];
-        memcpy(buffer, info.audioAddress.data(), info.audioAddress.size());
-        buffer[info.audioAddress.size()] = '\0';
-        jstring audioAddress = env->NewStringUTF(reinterpret_cast<const char *>(buffer));
-        env->CallObjectMethod(
-                builder, gTvInputHardwareInfoBuilderClassInfo.audioAddress, audioAddress);
-        env->DeleteLocalRef(audioAddress);
-    }
-
-    jobject infoObject = env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.build);
-
-    env->CallVoidMethod(
-            mThiz,
-            gTvInputHalClassInfo.deviceAvailable,
-            infoObject);
-
-    env->DeleteLocalRef(builder);
-    env->DeleteLocalRef(infoObject);
-}
-
-void JTvInputHal::onDeviceUnavailable(int deviceId) {
-    {
-        Mutex::Autolock autoLock(&mLock);
-        KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
-        for (size_t i = 0; i < connections.size(); ++i) {
-            removeStream(deviceId, connections.keyAt(i));
-        }
-        connections.clear();
-        mConnections.removeItem(deviceId);
-    }
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->CallVoidMethod(
-            mThiz,
-            gTvInputHalClassInfo.deviceUnavailable,
-            deviceId);
-}
-
-void JTvInputHal::onStreamConfigurationsChanged(int deviceId, int cableConnectionStatus) {
-    {
-        Mutex::Autolock autoLock(&mLock);
-        KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
-        for (size_t i = 0; i < connections.size(); ++i) {
-            removeStream(deviceId, connections.keyAt(i));
-        }
-        connections.clear();
-    }
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->CallVoidMethod(mThiz, gTvInputHalClassInfo.streamConfigsChanged, deviceId,
-                        cableConnectionStatus);
-}
-
-void JTvInputHal::onCaptured(int deviceId, int streamId, uint32_t seq, bool succeeded) {
-    sp<BufferProducerThread> thread;
-    {
-        Mutex::Autolock autoLock(&mLock);
-        KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
-        Connection& connection = connections.editValueFor(streamId);
-        if (connection.mThread == NULL) {
-            ALOGE("capture thread not existing.");
-            return;
-        }
-        thread = connection.mThread;
-    }
-    thread->onCaptured(seq, succeeded);
-    if (seq == 0) {
-        JNIEnv* env = AndroidRuntime::getJNIEnv();
-        env->CallVoidMethod(
-                mThiz,
-                gTvInputHalClassInfo.firstFrameCaptured,
-                deviceId,
-                streamId);
-    }
-}
-
-JTvInputHal::NotifyHandler::NotifyHandler(JTvInputHal* hal, const TvInputEvent& event) {
-    mHal = hal;
-    mEvent = event;
-}
-
-void JTvInputHal::NotifyHandler::handleMessage(const Message& message) {
-    switch (mEvent.type) {
-        case TvInputEventType::DEVICE_AVAILABLE: {
-            mHal->onDeviceAvailable(mEvent.deviceInfo);
-        } break;
-        case TvInputEventType::DEVICE_UNAVAILABLE: {
-            mHal->onDeviceUnavailable(mEvent.deviceInfo.deviceId);
-        } break;
-        case TvInputEventType::STREAM_CONFIGURATIONS_CHANGED: {
-            int cableConnectionStatus = static_cast<int>(mEvent.deviceInfo.cableConnectionStatus);
-            mHal->onStreamConfigurationsChanged(mEvent.deviceInfo.deviceId, cableConnectionStatus);
-        } break;
-        default:
-            ALOGE("Unrecognizable event");
-    }
-}
-
-JTvInputHal::TvInputCallback::TvInputCallback(JTvInputHal* hal) {
-    mHal = hal;
-}
-
-Return<void> JTvInputHal::TvInputCallback::notify(const TvInputEvent& event) {
-    mHal->mLooper->sendMessage(new NotifyHandler(mHal, event), static_cast<int>(event.type));
-    return Void();
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
 static jlong nativeOpen(JNIEnv* env, jobject thiz, jobject messageQueueObj) {
     sp<MessageQueue> messageQueue =
             android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
@@ -617,7 +55,7 @@
 static jobjectArray nativeGetStreamConfigs(JNIEnv* env, jclass clazz,
         jlong ptr, jint deviceId, jint generation) {
     JTvInputHal* tvInputHal = (JTvInputHal*)ptr;
-    const hidl_vec<TvStreamConfig> configs = tvInputHal->getStreamConfigs(deviceId);
+    const std::vector<AidlTvStreamConfig> configs = tvInputHal->getStreamConfigs(deviceId);
 
     jobjectArray result = env->NewObjectArray(configs.size(), gTvStreamConfigClassInfo.clazz, NULL);
     for (size_t i = 0; i < configs.size(); ++i) {
diff --git a/services/core/jni/tvinput/BufferProducerThread.cpp b/services/core/jni/tvinput/BufferProducerThread.cpp
new file mode 100644
index 0000000..f39dcee
--- /dev/null
+++ b/services/core/jni/tvinput/BufferProducerThread.cpp
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "BufferProducerThread.h"
+
+namespace android {
+
+BufferProducerThread::BufferProducerThread(tv_input_device_t* device, int deviceId,
+                                           const tv_stream_t* stream)
+      : Thread(false),
+        mDevice(device),
+        mDeviceId(deviceId),
+        mBuffer(NULL),
+        mBufferState(RELEASED),
+        mSeq(0u),
+        mShutdown(false) {
+    memcpy(&mStream, stream, sizeof(mStream));
+}
+
+status_t BufferProducerThread::readyToRun() {
+    sp<ANativeWindow> anw(mSurface);
+    status_t err = native_window_set_usage(anw.get(), mStream.buffer_producer.usage);
+    if (err != NO_ERROR) {
+        return err;
+    }
+    err = native_window_set_buffers_dimensions(anw.get(), mStream.buffer_producer.width,
+                                               mStream.buffer_producer.height);
+    if (err != NO_ERROR) {
+        return err;
+    }
+    err = native_window_set_buffers_format(anw.get(), mStream.buffer_producer.format);
+    if (err != NO_ERROR) {
+        return err;
+    }
+    return NO_ERROR;
+}
+
+void BufferProducerThread::setSurface(const sp<Surface>& surface) {
+    Mutex::Autolock autoLock(&mLock);
+    setSurfaceLocked(surface);
+}
+
+void BufferProducerThread::setSurfaceLocked(const sp<Surface>& surface) {
+    if (surface == mSurface) {
+        return;
+    }
+
+    if (mBufferState == CAPTURING) {
+        mDevice->cancel_capture(mDevice, mDeviceId, mStream.stream_id, mSeq);
+    }
+    while (mBufferState == CAPTURING) {
+        status_t err = mCondition.waitRelative(mLock, s2ns(1));
+        if (err != NO_ERROR) {
+            ALOGE("error %d while wating for buffer state to change.", err);
+            break;
+        }
+    }
+    mBuffer.clear();
+    mBufferState = RELEASED;
+
+    mSurface = surface;
+    mCondition.broadcast();
+}
+
+void BufferProducerThread::onCaptured(uint32_t seq, bool succeeded) {
+    Mutex::Autolock autoLock(&mLock);
+    if (seq != mSeq) {
+        ALOGW("Incorrect sequence value: expected %u actual %u", mSeq, seq);
+    }
+    if (mBufferState != CAPTURING) {
+        ALOGW("mBufferState != CAPTURING : instead %d", mBufferState);
+    }
+    if (succeeded) {
+        mBufferState = CAPTURED;
+    } else {
+        mBuffer.clear();
+        mBufferState = RELEASED;
+    }
+    mCondition.broadcast();
+}
+
+void BufferProducerThread::shutdown() {
+    Mutex::Autolock autoLock(&mLock);
+    mShutdown = true;
+    setSurfaceLocked(NULL);
+    requestExitAndWait();
+}
+
+bool BufferProducerThread::threadLoop() {
+    Mutex::Autolock autoLock(&mLock);
+
+    status_t err = NO_ERROR;
+    if (mSurface == NULL) {
+        err = mCondition.waitRelative(mLock, s2ns(1));
+        // It's OK to time out here.
+        if (err != NO_ERROR && err != TIMED_OUT) {
+            ALOGE("error %d while wating for non-null surface to be set", err);
+            return false;
+        }
+        return true;
+    }
+    sp<ANativeWindow> anw(mSurface);
+    while (mBufferState == CAPTURING) {
+        err = mCondition.waitRelative(mLock, s2ns(1));
+        if (err != NO_ERROR) {
+            ALOGE("error %d while wating for buffer state to change.", err);
+            return false;
+        }
+    }
+    if (mBufferState == CAPTURED && anw != NULL) {
+        err = anw->queueBuffer(anw.get(), mBuffer.get(), -1);
+        if (err != NO_ERROR) {
+            ALOGE("error %d while queueing buffer to surface", err);
+            return false;
+        }
+        mBuffer.clear();
+        mBufferState = RELEASED;
+    }
+    if (mBuffer == NULL && !mShutdown && anw != NULL) {
+        ANativeWindowBuffer_t* buffer = NULL;
+        err = native_window_dequeue_buffer_and_wait(anw.get(), &buffer);
+        if (err != NO_ERROR) {
+            ALOGE("error %d while dequeueing buffer to surface", err);
+            return false;
+        }
+        mBuffer = buffer;
+        mBufferState = CAPTURING;
+        mDevice->request_capture(mDevice, mDeviceId, mStream.stream_id, buffer->handle, ++mSeq);
+    }
+
+    return true;
+}
+
+} // namespace android
diff --git a/services/core/jni/tvinput/BufferProducerThread.h b/services/core/jni/tvinput/BufferProducerThread.h
new file mode 100644
index 0000000..2e0fa91
--- /dev/null
+++ b/services/core/jni/tvinput/BufferProducerThread.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <gui/Surface.h>
+#include <hardware/tv_input.h>
+#include <utils/Thread.h>
+
+namespace android {
+
+class BufferProducerThread : public Thread {
+public:
+    BufferProducerThread(tv_input_device_t* device, int deviceId, const tv_stream_t* stream);
+
+    virtual status_t readyToRun();
+
+    void setSurface(const sp<Surface>& surface);
+    void onCaptured(uint32_t seq, bool succeeded);
+    void shutdown();
+
+private:
+    Mutex mLock;
+    Condition mCondition;
+    sp<Surface> mSurface;
+    tv_input_device_t* mDevice;
+    int mDeviceId;
+    tv_stream_t mStream;
+    sp<ANativeWindowBuffer_t> mBuffer;
+    enum {
+        CAPTURING,
+        CAPTURED,
+        RELEASED,
+    } mBufferState;
+    uint32_t mSeq;
+    bool mShutdown;
+
+    virtual bool threadLoop();
+
+    void setSurfaceLocked(const sp<Surface>& surface);
+};
+
+} // namespace android
diff --git a/services/core/jni/tvinput/JTvInputHal.cpp b/services/core/jni/tvinput/JTvInputHal.cpp
new file mode 100644
index 0000000..0f6ef03
--- /dev/null
+++ b/services/core/jni/tvinput/JTvInputHal.cpp
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "JTvInputHal.h"
+
+namespace android {
+
+JTvInputHal::JTvInputHal(JNIEnv* env, jobject thiz, std::shared_ptr<ITvInputWrapper> tvInput,
+                         const sp<Looper>& looper) {
+    mThiz = env->NewWeakGlobalRef(thiz);
+    mTvInput = tvInput;
+    mLooper = looper;
+    mTvInputCallback = ::ndk::SharedRefBase::make<TvInputCallback>(this);
+    mTvInput->setCallback(mTvInputCallback);
+}
+
+JTvInputHal::~JTvInputHal() {
+    mTvInput->setCallback(nullptr);
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->DeleteWeakGlobalRef(mThiz);
+    mThiz = NULL;
+}
+
+JTvInputHal* JTvInputHal::createInstance(JNIEnv* env, jobject thiz, const sp<Looper>& looper) {
+    sp<HidlITvInput> hidlITvInput = HidlITvInput::getService();
+    if (hidlITvInput != nullptr) {
+        ALOGD("tv.input service is HIDL.");
+        return new JTvInputHal(env, thiz,
+                               std::shared_ptr<ITvInputWrapper>(new ITvInputWrapper(hidlITvInput)),
+                               looper);
+    }
+    ::ndk::SpAIBinder binder(AServiceManager_waitForService(TV_INPUT_AIDL_SERVICE_NAME));
+    std::shared_ptr<AidlITvInput> aidlITvInput = AidlITvInput::fromBinder(binder);
+    if (aidlITvInput == nullptr) {
+        ALOGE("Couldn't get tv.input service.");
+        return nullptr;
+    }
+    return new JTvInputHal(env, thiz,
+                           std::shared_ptr<ITvInputWrapper>(new ITvInputWrapper(aidlITvInput)),
+                           looper);
+}
+
+int JTvInputHal::addOrUpdateStream(int deviceId, int streamId, const sp<Surface>& surface) {
+    Mutex::Autolock autoLock(&mStreamLock);
+    KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
+    if (connections.indexOfKey(streamId) < 0) {
+        connections.add(streamId, Connection());
+    }
+    Connection& connection = connections.editValueFor(streamId);
+    if (connection.mSurface == surface) {
+        // Nothing to do
+        return NO_ERROR;
+    }
+    // Clear the surface in the connection.
+    if (connection.mSurface != NULL) {
+        if (connection.mStreamType == TV_STREAM_TYPE_INDEPENDENT_VIDEO_SOURCE) {
+            if (Surface::isValid(connection.mSurface)) {
+                connection.mSurface->setSidebandStream(NULL);
+            }
+        }
+        connection.mSurface.clear();
+    }
+    if (connection.mSourceHandle == NULL && connection.mThread == NULL) {
+        // Need to configure stream
+        ::ndk::ScopedAStatus status;
+        std::vector<AidlTvStreamConfig> list;
+        status = mTvInput->getStreamConfigurations(deviceId, &list);
+        if (!status.isOk()) {
+            ALOGE("Couldn't get stream configs for device id:%d result:%d", deviceId,
+                  status.getServiceSpecificError());
+            return UNKNOWN_ERROR;
+        }
+        int configIndex = -1;
+        for (size_t i = 0; i < list.size(); ++i) {
+            if (list[i].streamId == streamId) {
+                configIndex = i;
+                break;
+            }
+        }
+        if (configIndex == -1) {
+            ALOGE("Cannot find a config with given stream ID: %d", streamId);
+            return BAD_VALUE;
+        }
+        connection.mStreamType = TV_STREAM_TYPE_INDEPENDENT_VIDEO_SOURCE;
+
+        AidlNativeHandle sidebandStream;
+        status = mTvInput->openStream(deviceId, streamId, &sidebandStream);
+        if (!status.isOk()) {
+            ALOGE("Couldn't open stream. device id:%d stream id:%d result:%d", deviceId, streamId,
+                  status.getServiceSpecificError());
+            return UNKNOWN_ERROR;
+        }
+        connection.mSourceHandle = NativeHandle::create(makeFromAidl(sidebandStream), true);
+    }
+    connection.mSurface = surface;
+    if (connection.mSurface != nullptr) {
+        connection.mSurface->setSidebandStream(connection.mSourceHandle);
+    }
+    return NO_ERROR;
+}
+
+int JTvInputHal::removeStream(int deviceId, int streamId) {
+    Mutex::Autolock autoLock(&mStreamLock);
+    KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
+    if (connections.indexOfKey(streamId) < 0) {
+        return BAD_VALUE;
+    }
+    Connection& connection = connections.editValueFor(streamId);
+    if (connection.mSurface == NULL) {
+        // Nothing to do
+        return NO_ERROR;
+    }
+    if (Surface::isValid(connection.mSurface)) {
+        connection.mSurface->setSidebandStream(NULL);
+    }
+    connection.mSurface.clear();
+    if (connection.mThread != NULL) {
+        connection.mThread->shutdown();
+        connection.mThread.clear();
+    }
+    if (!mTvInput->closeStream(deviceId, streamId).isOk()) {
+        ALOGE("Couldn't close stream. device id:%d stream id:%d", deviceId, streamId);
+        return BAD_VALUE;
+    }
+    if (connection.mSourceHandle != NULL) {
+        connection.mSourceHandle.clear();
+    }
+    return NO_ERROR;
+}
+
+const std::vector<AidlTvStreamConfig> JTvInputHal::getStreamConfigs(int deviceId) {
+    std::vector<AidlTvStreamConfig> list;
+    ::ndk::ScopedAStatus status = mTvInput->getStreamConfigurations(deviceId, &list);
+    if (!status.isOk()) {
+        ALOGE("Couldn't get stream configs for device id:%d result:%d", deviceId,
+              status.getServiceSpecificError());
+        return std::vector<AidlTvStreamConfig>();
+    }
+    return list;
+}
+
+void JTvInputHal::onDeviceAvailable(const TvInputDeviceInfoWrapper& info) {
+    {
+        Mutex::Autolock autoLock(&mLock);
+        mConnections.add(info.deviceId, KeyedVector<int, Connection>());
+    }
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    jobject builder = env->NewObject(gTvInputHardwareInfoBuilderClassInfo.clazz,
+                                     gTvInputHardwareInfoBuilderClassInfo.constructor);
+    env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.deviceId, info.deviceId);
+    env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.type, info.type);
+    if (info.type == TvInputType::HDMI) {
+        env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.hdmiPortId,
+                              info.portId);
+    }
+    env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.cableConnectionStatus,
+                          info.cableConnectionStatus);
+    if (info.isHidl) {
+        hidlSetUpAudioInfo(env, builder, info);
+    } else {
+        AidlAudioDeviceType audioType = info.aidlAudioDevice.type.type;
+        env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.audioType, audioType);
+        if (audioType != AidlAudioDeviceType::NONE) {
+            std::stringstream ss;
+            switch (info.aidlAudioDevice.address.getTag()) {
+                case AidlAudioDeviceAddress::id:
+                    ss << info.aidlAudioDevice.address.get<AidlAudioDeviceAddress::id>();
+                    break;
+                case AidlAudioDeviceAddress::mac: {
+                    std::vector<uint8_t> addrList =
+                            info.aidlAudioDevice.address.get<AidlAudioDeviceAddress::mac>();
+                    for (int i = 0; i < addrList.size(); i++) {
+                        if (i != 0) {
+                            ss << ':';
+                        }
+                        ss << std::uppercase << std::setfill('0') << std::setw(2) << std::hex
+                           << static_cast<int32_t>(addrList[i]);
+                    }
+                } break;
+                case AidlAudioDeviceAddress::ipv4: {
+                    std::vector<uint8_t> addrList =
+                            info.aidlAudioDevice.address.get<AidlAudioDeviceAddress::ipv4>();
+                    for (int i = 0; i < addrList.size(); i++) {
+                        if (i != 0) {
+                            ss << '.';
+                        }
+                        ss << static_cast<int32_t>(addrList[i]);
+                    }
+                } break;
+                case AidlAudioDeviceAddress::ipv6: {
+                    std::vector<int32_t> addrList =
+                            info.aidlAudioDevice.address.get<AidlAudioDeviceAddress::ipv6>();
+                    for (int i = 0; i < addrList.size(); i++) {
+                        if (i != 0) {
+                            ss << ':';
+                        }
+                        ss << std::uppercase << std::setfill('0') << std::setw(4) << std::hex
+                           << addrList[i];
+                    }
+                } break;
+                case AidlAudioDeviceAddress::alsa: {
+                    std::vector<int32_t> addrList =
+                            info.aidlAudioDevice.address.get<AidlAudioDeviceAddress::alsa>();
+                    ss << "card=" << addrList[0] << ";device=" << addrList[1];
+                } break;
+            }
+            std::string bufferStr = ss.str();
+            jstring audioAddress = env->NewStringUTF(bufferStr.c_str());
+            env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.audioAddress,
+                                  audioAddress);
+            env->DeleteLocalRef(audioAddress);
+        }
+    }
+
+    jobject infoObject = env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.build);
+
+    env->CallVoidMethod(mThiz, gTvInputHalClassInfo.deviceAvailable, infoObject);
+
+    env->DeleteLocalRef(builder);
+    env->DeleteLocalRef(infoObject);
+}
+
+void JTvInputHal::onDeviceUnavailable(int deviceId) {
+    {
+        Mutex::Autolock autoLock(&mLock);
+        KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
+        for (size_t i = 0; i < connections.size(); ++i) {
+            removeStream(deviceId, connections.keyAt(i));
+        }
+        connections.clear();
+        mConnections.removeItem(deviceId);
+    }
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->CallVoidMethod(mThiz, gTvInputHalClassInfo.deviceUnavailable, deviceId);
+}
+
+void JTvInputHal::onStreamConfigurationsChanged(int deviceId, int cableConnectionStatus) {
+    {
+        Mutex::Autolock autoLock(&mLock);
+        KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
+        for (size_t i = 0; i < connections.size(); ++i) {
+            removeStream(deviceId, connections.keyAt(i));
+        }
+        connections.clear();
+    }
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->CallVoidMethod(mThiz, gTvInputHalClassInfo.streamConfigsChanged, deviceId,
+                        cableConnectionStatus);
+}
+
+void JTvInputHal::onCaptured(int deviceId, int streamId, uint32_t seq, bool succeeded) {
+    sp<BufferProducerThread> thread;
+    {
+        Mutex::Autolock autoLock(&mLock);
+        KeyedVector<int, Connection>& connections = mConnections.editValueFor(deviceId);
+        Connection& connection = connections.editValueFor(streamId);
+        if (connection.mThread == NULL) {
+            ALOGE("capture thread not existing.");
+            return;
+        }
+        thread = connection.mThread;
+    }
+    thread->onCaptured(seq, succeeded);
+    if (seq == 0) {
+        JNIEnv* env = AndroidRuntime::getJNIEnv();
+        env->CallVoidMethod(mThiz, gTvInputHalClassInfo.firstFrameCaptured, deviceId, streamId);
+    }
+}
+
+JTvInputHal::TvInputDeviceInfoWrapper
+JTvInputHal::TvInputDeviceInfoWrapper::createDeviceInfoWrapper(
+        const AidlTvInputDeviceInfo& aidlTvInputDeviceInfo) {
+    TvInputDeviceInfoWrapper deviceInfo;
+    deviceInfo.isHidl = false;
+    deviceInfo.deviceId = aidlTvInputDeviceInfo.deviceId;
+    deviceInfo.type = aidlTvInputDeviceInfo.type;
+    deviceInfo.portId = aidlTvInputDeviceInfo.portId;
+    deviceInfo.cableConnectionStatus = aidlTvInputDeviceInfo.cableConnectionStatus;
+    deviceInfo.aidlAudioDevice = aidlTvInputDeviceInfo.audioDevice;
+    return deviceInfo;
+}
+
+JTvInputHal::TvInputEventWrapper JTvInputHal::TvInputEventWrapper::createEventWrapper(
+        const AidlTvInputEvent& aidlTvInputEvent) {
+    TvInputEventWrapper event;
+    event.type = aidlTvInputEvent.type;
+    event.deviceInfo =
+            TvInputDeviceInfoWrapper::createDeviceInfoWrapper(aidlTvInputEvent.deviceInfo);
+    return event;
+}
+
+JTvInputHal::NotifyHandler::NotifyHandler(JTvInputHal* hal, const TvInputEventWrapper& event) {
+    mHal = hal;
+    mEvent = event;
+}
+
+void JTvInputHal::NotifyHandler::handleMessage(const Message& message) {
+    switch (mEvent.type) {
+        case TvInputEventType::DEVICE_AVAILABLE: {
+            mHal->onDeviceAvailable(mEvent.deviceInfo);
+        } break;
+        case TvInputEventType::DEVICE_UNAVAILABLE: {
+            mHal->onDeviceUnavailable(mEvent.deviceInfo.deviceId);
+        } break;
+        case TvInputEventType::STREAM_CONFIGURATIONS_CHANGED: {
+            int cableConnectionStatus = static_cast<int>(mEvent.deviceInfo.cableConnectionStatus);
+            mHal->onStreamConfigurationsChanged(mEvent.deviceInfo.deviceId, cableConnectionStatus);
+        } break;
+        default:
+            ALOGE("Unrecognizable event");
+    }
+}
+
+JTvInputHal::TvInputCallback::TvInputCallback(JTvInputHal* hal) {
+    mHal = hal;
+}
+
+::ndk::ScopedAStatus JTvInputHal::TvInputCallback::notify(const AidlTvInputEvent& event) {
+    mHal->mLooper->sendMessage(new NotifyHandler(mHal,
+                                                 TvInputEventWrapper::createEventWrapper(event)),
+                               static_cast<int>(event.type));
+    return ::ndk::ScopedAStatus::ok();
+}
+
+JTvInputHal::ITvInputWrapper::ITvInputWrapper(std::shared_ptr<AidlITvInput>& aidlTvInput)
+      : mIsHidl(false), mAidlTvInput(aidlTvInput) {}
+
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::setCallback(
+        const std::shared_ptr<TvInputCallback>& in_callback) {
+    if (mIsHidl) {
+        return hidlSetCallback(in_callback);
+    } else {
+        return mAidlTvInput->setCallback(in_callback);
+    }
+}
+
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::getStreamConfigurations(
+        int32_t in_deviceId, std::vector<AidlTvStreamConfig>* _aidl_return) {
+    if (mIsHidl) {
+        return hidlGetStreamConfigurations(in_deviceId, _aidl_return);
+    } else {
+        return mAidlTvInput->getStreamConfigurations(in_deviceId, _aidl_return);
+    }
+}
+
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::openStream(int32_t in_deviceId,
+                                                              int32_t in_streamId,
+                                                              AidlNativeHandle* _aidl_return) {
+    if (mIsHidl) {
+        return hidlOpenStream(in_deviceId, in_streamId, _aidl_return);
+    } else {
+        return mAidlTvInput->openStream(in_deviceId, in_streamId, _aidl_return);
+    }
+}
+
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::closeStream(int32_t in_deviceId,
+                                                               int32_t in_streamId) {
+    if (mIsHidl) {
+        return hidlCloseStream(in_deviceId, in_streamId);
+    } else {
+        return mAidlTvInput->closeStream(in_deviceId, in_streamId);
+    }
+}
+
+} // namespace android
diff --git a/services/core/jni/tvinput/JTvInputHal.h b/services/core/jni/tvinput/JTvInputHal.h
new file mode 100644
index 0000000..2697294
--- /dev/null
+++ b/services/core/jni/tvinput/JTvInputHal.h
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#define TV_INPUT_AIDL_SERVICE_NAME "android.hardware.tv.input.ITvInput/default"
+
+#include <aidl/android/hardware/tv/input/BnTvInputCallback.h>
+#include <aidl/android/hardware/tv/input/CableConnectionStatus.h>
+#include <aidl/android/hardware/tv/input/ITvInput.h>
+#include <aidl/android/media/audio/common/AudioDevice.h>
+#include <aidlcommonsupport/NativeHandle.h>
+#include <android/binder_manager.h>
+#include <nativehelper/JNIHelp.h>
+#include <utils/Errors.h>
+#include <utils/KeyedVector.h>
+#include <utils/Log.h>
+#include <utils/Looper.h>
+#include <utils/NativeHandle.h>
+
+#include <iomanip>
+
+#include "BufferProducerThread.h"
+#include "TvInputHal_hidl.h"
+#include "android_os_MessageQueue.h"
+#include "android_runtime/AndroidRuntime.h"
+#include "android_runtime/android_view_Surface.h"
+#include "tvinput/jstruct.h"
+
+using ::aidl::android::hardware::tv::input::BnTvInputCallback;
+using ::aidl::android::hardware::tv::input::CableConnectionStatus;
+using ::aidl::android::hardware::tv::input::TvInputEventType;
+using ::aidl::android::hardware::tv::input::TvInputType;
+
+using AidlAudioDevice = ::aidl::android::media::audio::common::AudioDevice;
+using AidlAudioDeviceAddress = ::aidl::android::media::audio::common::AudioDeviceAddress;
+using AidlAudioDeviceType = ::aidl::android::media::audio::common::AudioDeviceType;
+using AidlITvInput = ::aidl::android::hardware::tv::input::ITvInput;
+using AidlNativeHandle = ::aidl::android::hardware::common::NativeHandle;
+using AidlTvInputDeviceInfo = ::aidl::android::hardware::tv::input::TvInputDeviceInfo;
+using AidlTvInputEvent = ::aidl::android::hardware::tv::input::TvInputEvent;
+using AidlTvStreamConfig = ::aidl::android::hardware::tv::input::TvStreamConfig;
+
+extern gTvInputHalClassInfoType gTvInputHalClassInfo;
+extern gTvStreamConfigClassInfoType gTvStreamConfigClassInfo;
+extern gTvStreamConfigBuilderClassInfoType gTvStreamConfigBuilderClassInfo;
+extern gTvInputHardwareInfoBuilderClassInfoType gTvInputHardwareInfoBuilderClassInfo;
+
+namespace android {
+
+class JTvInputHal {
+public:
+    ~JTvInputHal();
+
+    static JTvInputHal* createInstance(JNIEnv* env, jobject thiz, const sp<Looper>& looper);
+
+    int addOrUpdateStream(int deviceId, int streamId, const sp<Surface>& surface);
+    int removeStream(int deviceId, int streamId);
+    const std::vector<AidlTvStreamConfig> getStreamConfigs(int deviceId);
+
+private:
+    // Connection between a surface and a stream.
+    class Connection {
+    public:
+        Connection() {}
+
+        sp<Surface> mSurface;
+        tv_stream_type_t mStreamType;
+
+        // Only valid when mStreamType == TV_STREAM_TYPE_INDEPENDENT_VIDEO_SOURCE
+        sp<NativeHandle> mSourceHandle;
+        // Only valid when mStreamType == TV_STREAM_TYPE_BUFFER_PRODUCER
+        sp<BufferProducerThread> mThread;
+    };
+
+    class TvInputDeviceInfoWrapper {
+    public:
+        TvInputDeviceInfoWrapper() {}
+
+        static TvInputDeviceInfoWrapper createDeviceInfoWrapper(
+                const AidlTvInputDeviceInfo& aidlTvInputDeviceInfo);
+        static TvInputDeviceInfoWrapper createDeviceInfoWrapper(
+                const HidlTvInputDeviceInfo& hidlTvInputDeviceInfo);
+
+        bool isHidl;
+        int deviceId;
+        TvInputType type;
+        int portId;
+        CableConnectionStatus cableConnectionStatus;
+        AidlAudioDevice aidlAudioDevice;
+        HidlAudioDevice hidlAudioType;
+        ::android::hardware::hidl_array<uint8_t, 32> hidlAudioAddress;
+    };
+
+    class TvInputEventWrapper {
+    public:
+        TvInputEventWrapper() {}
+
+        static TvInputEventWrapper createEventWrapper(const AidlTvInputEvent& aidlTvInputEvent);
+        static TvInputEventWrapper createEventWrapper(const HidlTvInputEvent& hidlTvInputEvent);
+
+        TvInputEventType type;
+        TvInputDeviceInfoWrapper deviceInfo;
+    };
+
+    class NotifyHandler : public MessageHandler {
+    public:
+        NotifyHandler(JTvInputHal* hal, const TvInputEventWrapper& event);
+
+        void handleMessage(const Message& message) override;
+
+    private:
+        TvInputEventWrapper mEvent;
+        JTvInputHal* mHal;
+    };
+
+    class TvInputCallback : public HidlITvInputCallback, public BnTvInputCallback {
+    public:
+        explicit TvInputCallback(JTvInputHal* hal);
+        ::ndk::ScopedAStatus notify(const AidlTvInputEvent& event) override;
+        Return<void> notify(const HidlTvInputEvent& event) override;
+
+    private:
+        JTvInputHal* mHal;
+    };
+
+    class ITvInputWrapper {
+    public:
+        ITvInputWrapper(std::shared_ptr<AidlITvInput>& aidlTvInput);
+        ITvInputWrapper(sp<HidlITvInput>& hidlTvInput);
+
+        ::ndk::ScopedAStatus setCallback(const std::shared_ptr<TvInputCallback>& in_callback);
+        ::ndk::ScopedAStatus getStreamConfigurations(int32_t in_deviceId,
+                                                     std::vector<AidlTvStreamConfig>* _aidl_return);
+        ::ndk::ScopedAStatus openStream(int32_t in_deviceId, int32_t in_streamId,
+                                        AidlNativeHandle* _aidl_return);
+        ::ndk::ScopedAStatus closeStream(int32_t in_deviceId, int32_t in_streamId);
+
+    private:
+        ::ndk::ScopedAStatus hidlSetCallback(const std::shared_ptr<TvInputCallback>& in_callback);
+        ::ndk::ScopedAStatus hidlGetStreamConfigurations(
+                int32_t in_deviceId, std::vector<AidlTvStreamConfig>* _aidl_return);
+        ::ndk::ScopedAStatus hidlOpenStream(int32_t in_deviceId, int32_t in_streamId,
+                                            AidlNativeHandle* _aidl_return);
+        ::ndk::ScopedAStatus hidlCloseStream(int32_t in_deviceId, int32_t in_streamId);
+
+        bool mIsHidl;
+        sp<HidlITvInput> mHidlTvInput;
+        std::shared_ptr<AidlITvInput> mAidlTvInput;
+    };
+
+    JTvInputHal(JNIEnv* env, jobject thiz, std::shared_ptr<ITvInputWrapper> tvInput,
+                const sp<Looper>& looper);
+
+    void hidlSetUpAudioInfo(JNIEnv* env, jobject& builder, const TvInputDeviceInfoWrapper& info);
+    void onDeviceAvailable(const TvInputDeviceInfoWrapper& info);
+    void onDeviceUnavailable(int deviceId);
+    void onStreamConfigurationsChanged(int deviceId, int cableConnectionStatus);
+    void onCaptured(int deviceId, int streamId, uint32_t seq, bool succeeded);
+
+    Mutex mLock;
+    Mutex mStreamLock;
+    jweak mThiz;
+    sp<Looper> mLooper;
+
+    KeyedVector<int, KeyedVector<int, Connection> > mConnections;
+
+    std::shared_ptr<ITvInputWrapper> mTvInput;
+    std::shared_ptr<TvInputCallback> mTvInputCallback;
+};
+
+} // namespace android
diff --git a/services/core/jni/tvinput/OWNERS b/services/core/jni/tvinput/OWNERS
new file mode 100644
index 0000000..adc5827
--- /dev/null
+++ b/services/core/jni/tvinput/OWNERS
@@ -0,0 +1,3 @@
+include /media/java/android/media/tv/OWNERS
+
+yixiaoluo@google.com
diff --git a/services/core/jni/tvinput/TvInputHal_hidl.cpp b/services/core/jni/tvinput/TvInputHal_hidl.cpp
new file mode 100644
index 0000000..37cf844
--- /dev/null
+++ b/services/core/jni/tvinput/TvInputHal_hidl.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "JTvInputHal.h"
+
+// Implement all HIDL related functions here.
+
+namespace android {
+
+void JTvInputHal::hidlSetUpAudioInfo(JNIEnv* env, jobject& builder,
+                                     const TvInputDeviceInfoWrapper& info) {
+    env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.audioType,
+                          info.hidlAudioType);
+    if (info.hidlAudioType != HidlAudioDevice::NONE) {
+        uint8_t buffer[info.hidlAudioAddress.size() + 1];
+        memcpy(buffer, info.hidlAudioAddress.data(), info.hidlAudioAddress.size());
+        buffer[info.hidlAudioAddress.size()] = '\0';
+        jstring audioAddress = env->NewStringUTF(reinterpret_cast<const char*>(buffer));
+        env->CallObjectMethod(builder, gTvInputHardwareInfoBuilderClassInfo.audioAddress,
+                              audioAddress);
+        env->DeleteLocalRef(audioAddress);
+    }
+}
+
+JTvInputHal::TvInputDeviceInfoWrapper
+JTvInputHal::TvInputDeviceInfoWrapper::createDeviceInfoWrapper(
+        const HidlTvInputDeviceInfo& hidlTvInputDeviceInfo) {
+    TvInputDeviceInfoWrapper deviceInfo;
+    deviceInfo.isHidl = true;
+    deviceInfo.deviceId = hidlTvInputDeviceInfo.deviceId;
+    deviceInfo.type = TvInputType(static_cast<int32_t>(hidlTvInputDeviceInfo.type));
+    deviceInfo.portId = hidlTvInputDeviceInfo.portId;
+    deviceInfo.cableConnectionStatus = CableConnectionStatus(
+            static_cast<int32_t>(hidlTvInputDeviceInfo.cableConnectionStatus));
+    deviceInfo.hidlAudioType = hidlTvInputDeviceInfo.audioType;
+    deviceInfo.hidlAudioAddress = hidlTvInputDeviceInfo.audioAddress;
+    return deviceInfo;
+}
+
+JTvInputHal::TvInputEventWrapper JTvInputHal::TvInputEventWrapper::createEventWrapper(
+        const HidlTvInputEvent& hidlTvInputEvent) {
+    TvInputEventWrapper event;
+    event.type = TvInputEventType(static_cast<int32_t>(hidlTvInputEvent.type));
+    event.deviceInfo =
+            TvInputDeviceInfoWrapper::createDeviceInfoWrapper(hidlTvInputEvent.deviceInfo);
+    return event;
+}
+
+Return<void> JTvInputHal::TvInputCallback::notify(const HidlTvInputEvent& event) {
+    mHal->mLooper->sendMessage(new NotifyHandler(mHal,
+                                                 TvInputEventWrapper::createEventWrapper(event)),
+                               static_cast<int>(event.type));
+    return Void();
+}
+
+JTvInputHal::ITvInputWrapper::ITvInputWrapper(sp<HidlITvInput>& hidlTvInput)
+      : mIsHidl(true), mHidlTvInput(hidlTvInput) {}
+
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::hidlSetCallback(
+        const std::shared_ptr<TvInputCallback>& in_callback) {
+    mHidlTvInput->setCallback(in_callback == nullptr ? nullptr
+                                                     : sp<TvInputCallback>(in_callback.get()));
+    return ::ndk::ScopedAStatus::ok();
+}
+
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::hidlGetStreamConfigurations(
+        int32_t in_deviceId, std::vector<AidlTvStreamConfig>* _aidl_return) {
+    Result result = Result::UNKNOWN;
+    hidl_vec<HidlTvStreamConfig> list;
+    mHidlTvInput->getStreamConfigurations(in_deviceId,
+                                          [&result, &list](Result res,
+                                                           hidl_vec<HidlTvStreamConfig> configs) {
+                                              result = res;
+                                              if (res == Result::OK) {
+                                                  list = configs;
+                                              }
+                                          });
+    if (result != Result::OK) {
+        ALOGE("Couldn't get stream configs for device id:%d result:%d", in_deviceId, result);
+        return ::ndk::ScopedAStatus::fromServiceSpecificError(static_cast<int32_t>(result));
+    }
+    for (size_t i = 0; i < list.size(); ++i) {
+        AidlTvStreamConfig config;
+        config.streamId = list[i].streamId;
+        config.maxVideoHeight = list[i].maxVideoHeight;
+        config.maxVideoWidth = list[i].maxVideoWidth;
+        _aidl_return->push_back(config);
+    }
+    return ::ndk::ScopedAStatus::ok();
+}
+
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::hidlOpenStream(int32_t in_deviceId,
+                                                                  int32_t in_streamId,
+                                                                  AidlNativeHandle* _aidl_return) {
+    Result result = Result::UNKNOWN;
+    native_handle_t* sidebandStream;
+    mHidlTvInput->openStream(in_deviceId, in_streamId,
+                             [&result, &sidebandStream](Result res, const native_handle_t* handle) {
+                                 result = res;
+                                 if (res == Result::OK) {
+                                     if (handle) {
+                                         sidebandStream = native_handle_clone(handle);
+                                     }
+                                 }
+                             });
+    if (result != Result::OK) {
+        ALOGE("Couldn't open stream. device id:%d stream id:%d result:%d", in_deviceId, in_streamId,
+              result);
+        return ::ndk::ScopedAStatus::fromServiceSpecificError(static_cast<int32_t>(result));
+    }
+    *_aidl_return = makeToAidl(sidebandStream);
+    native_handle_delete(sidebandStream);
+    return ::ndk::ScopedAStatus::ok();
+}
+
+::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::hidlCloseStream(int32_t in_deviceId,
+                                                                   int32_t in_streamId) {
+    Result result = mHidlTvInput->closeStream(in_deviceId, in_streamId);
+    if (result != Result::OK) {
+        return ::ndk::ScopedAStatus::fromServiceSpecificError(static_cast<int32_t>(result));
+    }
+    return ::ndk::ScopedAStatus::ok();
+}
+
+} // namespace android
diff --git a/services/core/jni/tvinput/TvInputHal_hidl.h b/services/core/jni/tvinput/TvInputHal_hidl.h
new file mode 100644
index 0000000..948f86b
--- /dev/null
+++ b/services/core/jni/tvinput/TvInputHal_hidl.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+// Include all HIDL related files/names here.
+
+#include <android/hardware/tv/input/1.0/ITvInput.h>
+#include <android/hardware/tv/input/1.0/ITvInputCallback.h>
+#include <android/hardware/tv/input/1.0/types.h>
+
+using ::android::hardware::hidl_vec;
+using ::android::hardware::Return;
+using ::android::hardware::Void;
+using ::android::hardware::tv::input::V1_0::Result;
+
+using HidlAudioDevice = ::android::hardware::audio::common::V2_0::AudioDevice;
+using HidlITvInput = ::android::hardware::tv::input::V1_0::ITvInput;
+using HidlITvInputCallback = ::android::hardware::tv::input::V1_0::ITvInputCallback;
+using HidlTvInputDeviceInfo = ::android::hardware::tv::input::V1_0::TvInputDeviceInfo;
+using HidlTvInputEvent = ::android::hardware::tv::input::V1_0::TvInputEvent;
+using HidlTvStreamConfig = ::android::hardware::tv::input::V1_0::TvStreamConfig;
diff --git a/services/core/jni/tvinput/jstruct.h b/services/core/jni/tvinput/jstruct.h
new file mode 100644
index 0000000..0a4a64d
--- /dev/null
+++ b/services/core/jni/tvinput/jstruct.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "jni.h"
+
+typedef struct {
+    jmethodID deviceAvailable;
+    jmethodID deviceUnavailable;
+    jmethodID streamConfigsChanged;
+    jmethodID firstFrameCaptured;
+} gTvInputHalClassInfoType;
+
+typedef struct {
+    jclass clazz;
+} gTvStreamConfigClassInfoType;
+
+typedef struct {
+    jclass clazz;
+
+    jmethodID constructor;
+    jmethodID streamId;
+    jmethodID type;
+    jmethodID maxWidth;
+    jmethodID maxHeight;
+    jmethodID generation;
+    jmethodID build;
+} gTvStreamConfigBuilderClassInfoType;
+
+typedef struct {
+    jclass clazz;
+
+    jmethodID constructor;
+    jmethodID deviceId;
+    jmethodID type;
+    jmethodID hdmiPortId;
+    jmethodID cableConnectionStatus;
+    jmethodID audioType;
+    jmethodID audioAddress;
+    jmethodID build;
+} gTvInputHardwareInfoBuilderClassInfoType;
diff --git a/services/core/xsd/display-layout-config/display-layout-config.xsd b/services/core/xsd/display-layout-config/display-layout-config.xsd
index e14139a..842d97a 100644
--- a/services/core/xsd/display-layout-config/display-layout-config.xsd
+++ b/services/core/xsd/display-layout-config/display-layout-config.xsd
@@ -50,6 +50,7 @@
     <xs:complexType name="display">
         <xs:sequence>
             <xs:element name="address" type="xs:nonNegativeInteger"/>
+            <xs:element name="position" type="xs:string" minOccurs="0" maxOccurs="1" />
         </xs:sequence>
         <xs:attribute name="enabled" type="xs:boolean" use="optional" />
         <xs:attribute name="defaultDisplay" type="xs:boolean" use="optional" />
diff --git a/services/core/xsd/display-layout-config/schema/current.txt b/services/core/xsd/display-layout-config/schema/current.txt
index f391575..55f866c 100644
--- a/services/core/xsd/display-layout-config/schema/current.txt
+++ b/services/core/xsd/display-layout-config/schema/current.txt
@@ -4,11 +4,13 @@
   public class Display {
     ctor public Display();
     method public java.math.BigInteger getAddress();
+    method public String getPosition();
     method public boolean isDefaultDisplay();
     method public boolean isEnabled();
     method public void setAddress(java.math.BigInteger);
     method public void setDefaultDisplay(boolean);
     method public void setEnabled(boolean);
+    method public void setPosition(String);
   }
 
   public class Layout {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c346b2f..e96eff21 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -211,6 +211,7 @@
 import com.android.server.utils.TimingsTraceAndSlog;
 import com.android.server.vibrator.VibratorManagerService;
 import com.android.server.vr.VrManagerService;
+import com.android.server.wearable.WearableSensingManagerService;
 import com.android.server.webkit.WebViewUpdateService;
 import com.android.server.wm.ActivityTaskManagerService;
 import com.android.server.wm.WindowManagerGlobalLock;
@@ -1830,6 +1831,7 @@
             startSystemCaptionsManagerService(context, t);
             startTextToSpeechManagerService(context, t);
             startAmbientContextService(t);
+            startWearableSensingService(t);
 
             // System Speech Recognition Service
             t.traceBegin("StartSpeechRecognitionManagerService");
@@ -3170,6 +3172,12 @@
         t.traceEnd();
     }
 
+    private void startWearableSensingService(@NonNull TimingsTraceAndSlog t) {
+        t.traceBegin("startWearableSensingService");
+        mSystemServiceManager.startService(WearableSensingManagerService.class);
+        t.traceEnd();
+    }
+
     private static void startSystemUi(Context context, WindowManagerService windowManager) {
         PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
         Intent intent = new Intent();
diff --git a/services/permission/Android.bp b/services/permission/Android.bp
index b03f17b..dc9b558 100644
--- a/services/permission/Android.bp
+++ b/services/permission/Android.bp
@@ -29,6 +29,8 @@
     ],
     static_libs: [
         "kotlin-stdlib",
+        // Adds reflection-less suppressed exceptions and AutoCloseable.use().
+        "kotlin-stdlib-jdk7",
     ],
     jarjar_rules: "jarjar-rules.txt",
     kotlincflags: [
diff --git a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
index c6ccc0f..7b96d42 100644
--- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
@@ -40,46 +40,53 @@
     }
 
     fun getDecision(subject: AccessUri, `object`: AccessUri): Int =
-        policy.getDecision(subject, `object`, state)
+        getState {
+            with(policy) { getDecision(subject, `object`) }
+        }
 
     fun setDecision(subject: AccessUri, `object`: AccessUri, decision: Int) {
-        mutateState { oldState, newState ->
-            policy.setDecision(subject, `object`, decision, oldState, newState)
+        mutateState {
+            with(policy) { setDecision(subject, `object`, decision) }
         }
     }
 
     fun onUserAdded(userId: Int) {
-        mutateState { oldState, newState ->
-            policy.onUserAdded(userId, oldState, newState)
+        mutateState {
+            with(policy) { onUserAdded(userId) }
         }
     }
 
     fun onUserRemoved(userId: Int) {
-        mutateState { oldState, newState ->
-            policy.onUserRemoved(userId, oldState, newState)
+        mutateState {
+            with(policy) { onUserRemoved(userId) }
         }
     }
 
     fun onPackageAdded(packageState: PackageState) {
-        mutateState { oldState, newState ->
-            policy.onPackageAdded(packageState, oldState, newState)
+        mutateState {
+            with(policy) { onPackageAdded(packageState) }
         }
     }
 
     fun onPackageRemoved(packageState: PackageState) {
-        mutateState { oldState, newState ->
-            policy.onPackageRemoved(packageState, oldState, newState)
+        mutateState {
+            with(policy) { onPackageRemoved(packageState) }
         }
     }
 
-    // TODO: Replace (oldState, newState) with Kotlin context receiver once it's stabilized.
-    private inline fun mutateState(action: (oldState: AccessState, newState: AccessState) -> Unit) {
+    internal inline fun <T> getState(action: GetStateScope.() -> T): T =
+        GetStateScope(state).action()
+
+    internal inline fun mutateState(action: MutateStateScope.() -> Unit) {
         synchronized(stateLock) {
             val oldState = state
             val newState = oldState.copy()
-            action(oldState, newState)
+            MutateStateScope(oldState, newState).action()
             persistence.write(newState)
             state = newState
         }
     }
+
+    internal fun getSchemePolicy(subjectScheme: String, objectScheme: String): SchemePolicy =
+        policy.getSchemePolicy(subjectScheme, objectScheme)
 }
diff --git a/services/permission/java/com/android/server/permission/access/AccessPersistence.kt b/services/permission/java/com/android/server/permission/access/AccessPersistence.kt
index 0efc1bd..022f09a 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPersistence.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPersistence.kt
@@ -44,13 +44,13 @@
         systemFile.parse {
             // This is the canonical way to call an extension function in a different class.
             // TODO(b/259469752): Use context receiver for this when it becomes stable.
-            with(policy) { this@parse.parseSystemState(systemState) }
+            with(policy) { parseSystemState(systemState) }
         }
     }
 
     private fun readUserState(userId: Int, userState: UserState) {
         getUserFile(userId).parse {
-            with(policy) { this@parse.parseUserState(userId, userState) }
+            with(policy) { parseUserState(userId, userState) }
         }
     }
 
@@ -82,13 +82,13 @@
 
     private fun writeSystemState(systemState: SystemState) {
         systemFile.serialize {
-            with(policy) { this@serialize.serializeSystemState(systemState) }
+            with(policy) { serializeSystemState(systemState) }
         }
     }
 
     private fun writeUserState(userId: Int, userState: UserState) {
         getUserFile(userId).serialize {
-            with(policy) { this@serialize.serializeUserState(userId, userState) }
+            with(policy) { serializeUserState(userId, userState) }
         }
     }
 
diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
index 4e53ce0..e9741c6 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
@@ -41,38 +41,35 @@
         }
     )
 
-    fun getDecision(subject: AccessUri, `object`: AccessUri, state: AccessState): Int =
-        getSchemePolicy(subject, `object`).getDecision(subject, `object`, state)
-
-    fun setDecision(
-        subject: AccessUri,
-        `object`: AccessUri,
-        decision: Int,
-        oldState: AccessState,
-        newState: AccessState
-    ) {
-        getSchemePolicy(subject, `object`)
-            .setDecision(subject, `object`, decision, oldState, newState)
-    }
-
-    private fun getSchemePolicy(subject: AccessUri, `object`: AccessUri): SchemePolicy =
-        checkNotNull(schemePolicies[subject.scheme]?.get(`object`.scheme)) {
-            "Scheme policy for subject=$subject object=$`object` does not exist"
+    fun getSchemePolicy(subjectScheme: String, objectScheme: String): SchemePolicy =
+        checkNotNull(schemePolicies[subjectScheme]?.get(objectScheme)) {
+            "Scheme policy for $subjectScheme and $objectScheme does not exist"
         }
 
-    fun onUserAdded(userId: Int, oldState: AccessState, newState: AccessState) {
+    fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int =
+        with(getSchemePolicy(subject, `object`)){ getDecision(subject, `object`) }
+
+    fun MutateStateScope.setDecision(subject: AccessUri, `object`: AccessUri, decision: Int) {
+        with(getSchemePolicy(subject, `object`)) { setDecision(subject, `object`, decision) }
+    }
+
+    fun MutateStateScope.onUserAdded(userId: Int) {
         newState.systemState.userIds += userId
         newState.userStates[userId] = UserState()
-        forEachSchemePolicy { it.onUserAdded(userId, oldState, newState) }
+        forEachSchemePolicy {
+            with(it) { onUserAdded(userId) }
+        }
     }
 
-    fun onUserRemoved(userId: Int, oldState: AccessState, newState: AccessState) {
+    fun MutateStateScope.onUserRemoved(userId: Int) {
         newState.systemState.userIds -= userId
         newState.userStates -= userId
-        forEachSchemePolicy { it.onUserRemoved(userId, oldState, newState) }
+        forEachSchemePolicy {
+            with(it) { onUserRemoved(userId) }
+        }
     }
 
-    fun onPackageAdded(packageState: PackageState, oldState: AccessState, newState: AccessState) {
+    fun MutateStateScope.onPackageAdded(packageState: PackageState) {
         var isAppIdAdded = false
         newState.systemState.apply {
             packageStates[packageState.packageName] = packageState
@@ -82,12 +79,16 @@
             }.add(packageState.packageName)
         }
         if (isAppIdAdded) {
-            forEachSchemePolicy { it.onAppIdAdded(packageState.appId, oldState, newState) }
+            forEachSchemePolicy {
+                with(it) { onAppIdAdded(packageState.appId) }
+            }
         }
-        forEachSchemePolicy { it.onPackageAdded(packageState, oldState, newState) }
+        forEachSchemePolicy {
+            with(it) { onPackageAdded(packageState) }
+        }
     }
 
-    fun onPackageRemoved(packageState: PackageState, oldState: AccessState, newState: AccessState) {
+    fun MutateStateScope.onPackageRemoved(packageState: PackageState) {
         var isAppIdRemoved = false
         newState.systemState.apply {
             packageStates -= packageState.packageName
@@ -101,9 +102,13 @@
                 }
             }
         }
-        forEachSchemePolicy { it.onPackageRemoved(packageState, oldState, newState) }
+        forEachSchemePolicy {
+            with(it) { onPackageRemoved(packageState) }
+        }
         if (isAppIdRemoved) {
-            forEachSchemePolicy { it.onAppIdRemoved(packageState.appId, oldState, newState) }
+            forEachSchemePolicy {
+                with(it) { onAppIdRemoved(packageState.appId) }
+            }
         }
     }
 
@@ -113,7 +118,7 @@
                 TAG_ACCESS -> {
                     forEachTag {
                         forEachSchemePolicy {
-                            with(it) { this@parseSystemState.parseSystemState(systemState) }
+                            with(it) { parseSystemState(systemState) }
                         }
                     }
                 }
@@ -125,7 +130,7 @@
     fun BinaryXmlSerializer.serializeSystemState(systemState: SystemState) {
         tag(TAG_ACCESS) {
             forEachSchemePolicy {
-                with(it) { this@serializeSystemState.serializeSystemState(systemState) }
+                with(it) { serializeSystemState(systemState) }
             }
         }
     }
@@ -136,7 +141,7 @@
                 TAG_ACCESS -> {
                     forEachTag {
                         forEachSchemePolicy {
-                            with(it) { this@parseUserState.parseUserState(userId, userState) }
+                            with(it) { parseUserState(userId, userState) }
                         }
                     }
                 }
@@ -153,11 +158,14 @@
     fun BinaryXmlSerializer.serializeUserState(userId: Int, userState: UserState) {
         tag(TAG_ACCESS) {
             forEachSchemePolicy {
-                with(it) { this@serializeUserState.serializeUserState(userId, userState) }
+                with(it) { serializeUserState(userId, userState) }
             }
         }
     }
 
+    private fun getSchemePolicy(subject: AccessUri, `object`: AccessUri): SchemePolicy =
+        getSchemePolicy(subject.scheme, `object`.scheme)
+
     private inline fun forEachSchemePolicy(action: (SchemePolicy) -> Unit) {
         schemePolicies.forEachValueIndexed { _, objectSchemePolicies ->
             objectSchemePolicies.forEachValueIndexed { _, schemePolicy ->
@@ -182,14 +190,12 @@
 
     abstract val objectScheme: String
 
-    abstract fun getDecision(subject: AccessUri, `object`: AccessUri, state: AccessState): Int
+    abstract fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int
 
-    abstract fun setDecision(
+    abstract fun MutateStateScope.setDecision(
         subject: AccessUri,
         `object`: AccessUri,
-        decision: Int,
-        oldState: AccessState,
-        newState: AccessState
+        decision: Int
     )
 
     fun addOnDecisionChangedListener(listener: OnDecisionChangedListener) {
@@ -216,25 +222,17 @@
         }
     }
 
-    open fun onUserAdded(userId: Int, oldState: AccessState, newState: AccessState) {}
+    open fun MutateStateScope.onUserAdded(userId: Int) {}
 
-    open fun onUserRemoved(userId: Int, oldState: AccessState, newState: AccessState) {}
+    open fun MutateStateScope.onUserRemoved(userId: Int) {}
 
-    open fun onAppIdAdded(appId: Int, oldState: AccessState, newState: AccessState) {}
+    open fun MutateStateScope.onAppIdAdded(appId: Int) {}
 
-    open fun onAppIdRemoved(appId: Int, oldState: AccessState, newState: AccessState) {}
+    open fun MutateStateScope.onAppIdRemoved(appId: Int) {}
 
-    open fun onPackageAdded(
-        packageState: PackageState,
-        oldState: AccessState,
-        newState: AccessState
-    ) {}
+    open fun MutateStateScope.onPackageAdded(packageState: PackageState) {}
 
-    open fun onPackageRemoved(
-        packageState: PackageState,
-        oldState: AccessState,
-        newState: AccessState
-    ) {}
+    open fun MutateStateScope.onPackageRemoved(packageState: PackageState) {}
 
     open fun BinaryXmlPullParser.parseSystemState(systemState: SystemState) {}
 
diff --git a/services/permission/java/com/android/server/permission/access/AccessState.kt b/services/permission/java/com/android/server/permission/access/AccessState.kt
index f496dbd..cf8f383 100644
--- a/services/permission/java/com/android/server/permission/access/AccessState.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessState.kt
@@ -124,3 +124,12 @@
         }
     }
 }
+
+class GetStateScope(
+    val state: AccessState
+)
+
+class MutateStateScope(
+    val oldState: AccessState,
+    val newState: AccessState
+)
diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpsCheckingServiceCompatImpl.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpsCheckingServiceCompatImpl.kt
new file mode 100644
index 0000000..a565feb
--- /dev/null
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpsCheckingServiceCompatImpl.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.permission.access.appop
+
+import android.util.ArraySet
+import android.util.SparseBooleanArray
+import android.util.SparseIntArray
+import com.android.server.appop.AppOpsCheckingServiceInterface
+import com.android.server.appop.OnOpModeChangedListener
+import com.android.server.permission.access.AccessCheckingService
+import java.io.PrintWriter
+
+class AppOpsCheckingServiceCompatImpl(
+    private val accessCheckingService: AccessCheckingService
+) : AppOpsCheckingServiceInterface {
+    override fun getNonDefaultUidModes(uid: Int): SparseIntArray {
+        TODO("Not yet implemented")
+    }
+
+    override fun getUidMode(uid: Int, op: Int): Int {
+        TODO("Not yet implemented")
+    }
+
+    override fun setUidMode(uid: Int, op: Int, mode: Int): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun getPackageMode(packageName: String, op: Int, userId: Int): Int {
+        TODO("Not yet implemented")
+    }
+
+    override fun setPackageMode(packageName: String, op: Int, mode: Int, userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun removePackage(packageName: String, userId: Int): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeUid(uid: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun areUidModesDefault(uid: Int): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun arePackageModesDefault(packageName: String, userId: Int): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun clearAllModes() {
+        TODO("Not yet implemented")
+    }
+
+    override fun startWatchingOpModeChanged(changedListener: OnOpModeChangedListener, op: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun startWatchingPackageModeChanged(
+        changedListener: OnOpModeChangedListener,
+        packageName: String
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeListener(changedListener: OnOpModeChangedListener) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getOpModeChangedListeners(op: Int): ArraySet<OnOpModeChangedListener> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getPackageModeChangedListeners(
+        packageName: String
+    ): ArraySet<OnOpModeChangedListener> {
+        TODO("Not yet implemented")
+    }
+
+    override fun notifyWatchersOfChange(op: Int, uid: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun notifyOpChanged(
+        changedListener: OnOpModeChangedListener,
+        op: Int,
+        uid: Int,
+        packageName: String?
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun notifyOpChangedForAllPkgsInUid(
+        op: Int,
+        uid: Int,
+        onlyForeground: Boolean,
+        callbackToIgnore: OnOpModeChangedListener?
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun evalForegroundUidOps(
+        uid: Int,
+        foregroundOps: SparseBooleanArray?
+    ): SparseBooleanArray {
+        TODO("Not yet implemented")
+    }
+
+    override fun evalForegroundPackageOps(
+        packageName: String,
+        foregroundOps: SparseBooleanArray?,
+        userId: Int
+    ): SparseBooleanArray {
+        TODO("Not yet implemented")
+    }
+
+    override fun dumpListeners(
+        dumpOp: Int,
+        dumpUid: Int,
+        dumpPackage: String?,
+        printWriter: PrintWriter
+    ): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    companion object {
+        private val LOG_TAG = AppOpsCheckingServiceCompatImpl::class.java.simpleName
+    }
+}
diff --git a/services/permission/java/com/android/server/permission/access/appop/BaseAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/BaseAppOpPolicy.kt
index 0b052f9..a1a5e2d 100644
--- a/services/permission/java/com/android/server/permission/access/appop/BaseAppOpPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/BaseAppOpPolicy.kt
@@ -19,44 +19,43 @@
 import android.app.AppOpsManager
 import com.android.modules.utils.BinaryXmlPullParser
 import com.android.modules.utils.BinaryXmlSerializer
-import com.android.server.permission.access.AccessState
 import com.android.server.permission.access.AccessUri
 import com.android.server.permission.access.AppOpUri
+import com.android.server.permission.access.GetStateScope
+import com.android.server.permission.access.MutateStateScope
 import com.android.server.permission.access.SchemePolicy
 import com.android.server.permission.access.UserState
 import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
 
 abstract class BaseAppOpPolicy(private val persistence: BaseAppOpPersistence) : SchemePolicy() {
-    override fun getDecision(subject: AccessUri, `object`: AccessUri, state: AccessState): Int {
+    override fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int {
         `object` as AppOpUri
-        return getModes(subject, state)
+        return getModes(subject)
             .getWithDefault(`object`.appOpName, opToDefaultMode(`object`.appOpName))
     }
 
-    override fun setDecision(
+    override fun MutateStateScope.setDecision(
         subject: AccessUri,
         `object`: AccessUri,
-        decision: Int,
-        oldState: AccessState,
-        newState: AccessState
+        decision: Int
     ) {
         `object` as AppOpUri
-        val modes = getOrCreateModes(subject, newState)
+        val modes = getOrCreateModes(subject)
         val oldMode = modes.putWithDefault(`object`.appOpName, decision,
             opToDefaultMode(`object`.appOpName))
         if (modes.isEmpty()) {
-            removeModes(subject, newState)
+            removeModes(subject)
         }
         if (oldMode != decision) {
             notifyOnDecisionChangedListeners(subject, `object`, oldMode, decision)
         }
     }
 
-    abstract fun getModes(subject: AccessUri, state: AccessState): IndexedMap<String, Int>?
+    abstract fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>?
 
-    abstract fun getOrCreateModes(subject: AccessUri, state: AccessState): IndexedMap<String, Int>
+    abstract fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int>
 
-    abstract fun removeModes(subject: AccessUri, state: AccessState)
+    abstract fun MutateStateScope.removeModes(subject: AccessUri)
 
     // TODO need to check that [AppOpsManager.getSystemAlertWindowDefault] works; likely no issue
     //  since running in system process.
diff --git a/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt
index 7c3c14c..966489f 100644
--- a/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt
@@ -16,9 +16,10 @@
 
 package com.android.server.permission.access.appop
 
-import com.android.server.permission.access.AccessState
 import com.android.server.permission.access.AccessUri
 import com.android.server.permission.access.AppOpUri
+import com.android.server.permission.access.GetStateScope
+import com.android.server.permission.access.MutateStateScope
 import com.android.server.permission.access.PackageUri
 import com.android.server.permission.access.UserState
 import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
@@ -31,27 +32,23 @@
     override val objectScheme: String
         get() = AppOpUri.SCHEME
 
-    override fun getModes(subject: AccessUri, state: AccessState): IndexedMap<String, Int>? {
+    override fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>? {
         subject as PackageUri
         return state.userStates[subject.userId]?.packageAppOpModes?.get(subject.packageName)
     }
 
-    override fun getOrCreateModes(subject: AccessUri, state: AccessState): IndexedMap<String, Int> {
+    override fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int> {
         subject as PackageUri
-        return state.userStates.getOrPut(subject.userId) { UserState() }
+        return newState.userStates.getOrPut(subject.userId) { UserState() }
             .packageAppOpModes.getOrPut(subject.packageName) { IndexedMap() }
     }
 
-    override fun removeModes(subject: AccessUri, state: AccessState) {
+    override fun MutateStateScope.removeModes(subject: AccessUri) {
         subject as PackageUri
-        state.userStates[subject.userId]?.packageAppOpModes?.remove(subject.packageName)
+        newState.userStates[subject.userId]?.packageAppOpModes?.remove(subject.packageName)
     }
 
-    override fun onPackageRemoved(
-        packageState: PackageState,
-        oldState: AccessState,
-        newState: AccessState
-    ) {
+    override fun MutateStateScope.onPackageRemoved(packageState: PackageState) {
         newState.userStates.forEachIndexed { _, _, userState ->
             userState.packageAppOpModes -= packageState.packageName
         }
diff --git a/services/permission/java/com/android/server/permission/access/appop/UidAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/UidAppOpPolicy.kt
index 26d0114..862db8f 100644
--- a/services/permission/java/com/android/server/permission/access/appop/UidAppOpPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/UidAppOpPolicy.kt
@@ -16,9 +16,10 @@
 
 package com.android.server.permission.access.appop
 
-import com.android.server.permission.access.AccessState
 import com.android.server.permission.access.AccessUri
 import com.android.server.permission.access.AppOpUri
+import com.android.server.permission.access.GetStateScope
+import com.android.server.permission.access.MutateStateScope
 import com.android.server.permission.access.UidUri
 import com.android.server.permission.access.UserState
 import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
@@ -30,23 +31,23 @@
     override val objectScheme: String
         get() = AppOpUri.SCHEME
 
-    override fun getModes(subject: AccessUri, state: AccessState): IndexedMap<String, Int>? {
+    override fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>? {
         subject as UidUri
         return state.userStates[subject.userId]?.uidAppOpModes?.get(subject.appId)
     }
 
-    override fun getOrCreateModes(subject: AccessUri, state: AccessState): IndexedMap<String, Int> {
+    override fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int> {
         subject as UidUri
-        return state.userStates.getOrPut(subject.userId) { UserState() }
+        return newState.userStates.getOrPut(subject.userId) { UserState() }
             .uidAppOpModes.getOrPut(subject.appId) { IndexedMap() }
     }
 
-    override fun removeModes(subject: AccessUri, state: AccessState) {
+    override fun MutateStateScope.removeModes(subject: AccessUri) {
         subject as UidUri
-        state.userStates[subject.userId]?.uidAppOpModes?.remove(subject.appId)
+        newState.userStates[subject.userId]?.uidAppOpModes?.remove(subject.appId)
     }
 
-    override fun onAppIdRemoved(appId: Int, oldState: AccessState, newState: AccessState) {
+    override fun MutateStateScope.onAppIdRemoved(appId: Int) {
         newState.userStates.forEachIndexed { _, _, userState ->
             userState.uidAppOpModes -= appId
         }
diff --git a/services/permission/java/com/android/server/permission/access/permission/ModernPermissionManagerServiceImpl.kt b/services/permission/java/com/android/server/permission/access/permission/ModernPermissionManagerServiceImpl.kt
new file mode 100644
index 0000000..cc51866
--- /dev/null
+++ b/services/permission/java/com/android/server/permission/access/permission/ModernPermissionManagerServiceImpl.kt
@@ -0,0 +1,456 @@
+/*
+ * 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.permission.access.permission
+
+import android.content.pm.PackageManager
+import android.content.pm.PackageManagerInternal
+import android.content.pm.PermissionGroupInfo
+import android.content.pm.PermissionInfo
+import android.content.pm.permission.SplitPermissionInfoParcelable
+import android.os.Binder
+import android.os.Build
+import android.os.Process
+import android.os.UserHandle
+import android.permission.IOnPermissionsChangeListener
+import com.android.server.LocalManagerRegistry
+import com.android.server.LocalServices
+import com.android.server.pm.PackageManagerLocal
+import com.android.server.pm.permission.PermissionManagerServiceInterface
+import com.android.server.permission.access.AccessCheckingService
+import com.android.server.permission.access.PermissionUri
+import com.android.server.permission.access.UidUri
+import com.android.server.permission.access.data.Permission
+import com.android.server.permission.access.util.hasBits
+import com.android.server.pm.permission.LegacyPermission
+import com.android.server.pm.permission.LegacyPermissionSettings
+import com.android.server.pm.permission.LegacyPermissionState
+import com.android.server.pm.permission.PermissionManagerServiceInternal
+import com.android.server.pm.pkg.AndroidPackage
+import java.io.FileDescriptor
+import java.io.PrintWriter
+
+/**
+ * Modern implementation of [PermissionManagerServiceInterface].
+ */
+class ModernPermissionManagerServiceImpl(
+    private val service: AccessCheckingService
+) : PermissionManagerServiceInterface {
+    private val policy =
+        service.getSchemePolicy(UidUri.SCHEME, PermissionUri.SCHEME) as UidPermissionPolicy
+
+    private val packageManagerInternal =
+        LocalServices.getService(PackageManagerInternal::class.java)
+
+    private val packageManagerLocal =
+        LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal::class.java)
+
+    override fun getAllPermissionGroups(flags: Int): List<PermissionGroupInfo> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getPermissionGroupInfo(
+        permissionGroupName: String,
+        flags: Int
+    ): PermissionGroupInfo? {
+        val permissionGroup: PermissionGroupInfo
+        packageManagerLocal.withUnfilteredSnapshot().use { snapshot ->
+            val callingUid = Binder.getCallingUid()
+            if (snapshot.isUidInstantApp(callingUid)) {
+                return null
+            }
+
+            permissionGroup = service.getState {
+                with(policy) { getPermissionGroup(permissionGroupName) }
+            } ?: return null
+
+            val isPermissionGroupVisible =
+                snapshot.isPackageVisibleToUid(permissionGroup.packageName, callingUid)
+            if (!isPermissionGroupVisible) {
+                return null
+            }
+        }
+
+        return permissionGroup.generatePermissionGroupInfo(flags)
+    }
+
+    /**
+     * Generate a new [PermissionGroupInfo] from [PermissionGroupInfo] and adjust it accordingly.
+     */
+    private fun PermissionGroupInfo.generatePermissionGroupInfo(flags: Int): PermissionGroupInfo =
+        @Suppress("DEPRECATION")
+        PermissionGroupInfo(this).apply {
+            if (!flags.hasBits(PackageManager.GET_META_DATA)) {
+                metaData = null
+            }
+        }
+
+    override fun getPermissionInfo(
+        permissionName: String,
+        flags: Int,
+        opPackageName: String
+    ): PermissionInfo? {
+        val permission: Permission
+        val targetSdkVersion: Int
+        packageManagerLocal.withUnfilteredSnapshot().use { snapshot ->
+            val callingUid = Binder.getCallingUid()
+            if (snapshot.isUidInstantApp(callingUid)) {
+                return null
+            }
+
+            permission = service.getState {
+                with(policy) { getPermission(permissionName) }
+            } ?: return null
+
+            val isPermissionVisible =
+                snapshot.isPackageVisibleToUid(permission.packageName, callingUid)
+            if (!isPermissionVisible) {
+                return null
+            }
+
+            val callingAppId = UserHandle.getAppId(callingUid)
+            val opPackage = snapshot.packageStates[opPackageName]?.androidPackage
+            targetSdkVersion = when {
+                // System sees all flags.
+                callingAppId == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID ||
+                    callingAppId == Process.SHELL_UID -> Build.VERSION_CODES.CUR_DEVELOPMENT
+                opPackage != null -> opPackage.targetSdkVersion
+                else -> Build.VERSION_CODES.CUR_DEVELOPMENT
+            }
+        }
+
+        return permission.generatePermissionInfo(flags, targetSdkVersion)
+    }
+
+    /**
+     * Generate a new [PermissionInfo] from [Permission] and adjust it accordingly.
+     */
+    private fun Permission.generatePermissionInfo(
+        flags: Int,
+        targetSdkVersion: Int
+    ): PermissionInfo =
+        @Suppress("DEPRECATION")
+        PermissionInfo(permissionInfo).apply {
+            if (!flags.hasBits(PackageManager.GET_META_DATA)) {
+                metaData = null
+            }
+            if (targetSdkVersion < Build.VERSION_CODES.O) {
+                val protection = protection
+                // Signature permission's protection flags are always reported.
+                if (protection != PermissionInfo.PROTECTION_SIGNATURE) {
+                    protectionLevel = protection
+                }
+            }
+        }
+
+    override fun queryPermissionsByGroup(
+        permissionGroupName: String,
+        flags: Int
+    ): List<PermissionInfo> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getAllPermissionsWithProtection(protection: Int): List<PermissionInfo> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getAllPermissionsWithProtectionFlags(protectionFlags: Int): List<PermissionInfo> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getPermissionGids(permissionName: String, userId: Int): IntArray {
+        TODO("Not yet implemented")
+    }
+
+    override fun addPermission(permissionInfo: PermissionInfo, async: Boolean): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun removePermission(permissionName: String) {
+        TODO("Not yet implemented")
+    }
+
+    override fun checkPermission(packageName: String, permissionName: String, userId: Int): Int {
+        TODO("Not yet implemented")
+    }
+
+    override fun checkUidPermission(uid: Int, permissionName: String): Int {
+        TODO("Not yet implemented")
+    }
+
+    override fun getGrantedPermissions(packageName: String, userId: Int): Set<String> {
+        TODO("Not yet implemented")
+    }
+
+    override fun grantRuntimePermission(packageName: String, permissionName: String, userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun revokeRuntimePermission(
+        packageName: String,
+        permissionName: String,
+        userId: Int,
+        reason: String?
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun revokePostNotificationPermissionWithoutKillForTest(
+        packageName: String,
+        userId: Int
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun addOnPermissionsChangeListener(listener: IOnPermissionsChangeListener) {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeOnPermissionsChangeListener(listener: IOnPermissionsChangeListener) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int {
+        TODO("Not yet implemented")
+    }
+
+    override fun isPermissionRevokedByPolicy(
+        packageName: String,
+        permissionName: String,
+        userId: Int
+    ): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun isPermissionsReviewRequired(packageName: String, userId: Int): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun shouldShowRequestPermissionRationale(
+        packageName: String,
+        permissionName: String,
+        userId: Int
+    ): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun updatePermissionFlags(
+        packageName: String,
+        permissionName: String,
+        flagMask: Int,
+        flagValues: Int,
+        checkAdjustPolicyFlagPermission: Boolean,
+        userId: Int
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun updatePermissionFlagsForAllApps(flagMask: Int, flagValues: Int, userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun addAllowlistedRestrictedPermission(
+        packageName: String,
+        permissionName: String,
+        flags: Int,
+        userId: Int
+    ): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun getAllowlistedRestrictedPermissions(
+        packageName: String,
+        flags: Int,
+        userId: Int
+    ): MutableList<String> {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeAllowlistedRestrictedPermission(
+        packageName: String,
+        permissionName: String,
+        flags: Int,
+        userId: Int
+    ): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun resetRuntimePermissions(androidPackage: AndroidPackage, userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun resetRuntimePermissionsForUser(userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun addOnRuntimePermissionStateChangedListener(
+        listener: PermissionManagerServiceInternal.OnRuntimePermissionStateChangedListener
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeOnRuntimePermissionStateChangedListener(
+        listener: PermissionManagerServiceInternal.OnRuntimePermissionStateChangedListener
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getSplitPermissions(): List<SplitPermissionInfoParcelable> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getAppOpPermissionPackages(permissionName: String): Array<String> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getAllAppOpPermissionPackages(): Map<String, Set<String>> {
+        TODO("Not yet implemented")
+    }
+
+    override fun getGidsForUid(uid: Int): IntArray {
+        TODO("Not yet implemented")
+    }
+
+    override fun backupRuntimePermissions(userId: Int): ByteArray? {
+        TODO("Not yet implemented")
+    }
+
+    override fun restoreRuntimePermissions(backup: ByteArray, userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun restoreDelayedRuntimePermissions(packageName: String, userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>?) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getPermissionTEMP(
+        permissionName: String
+    ): com.android.server.pm.permission.Permission? {
+        TODO("Not yet implemented")
+    }
+
+    override fun getLegacyPermissions(): List<LegacyPermission> {
+        TODO("Not yet implemented")
+    }
+
+    override fun readLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) {
+        TODO("Not yet implemented")
+    }
+
+    override fun writeLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getLegacyPermissionState(appId: Int): LegacyPermissionState {
+        TODO("Not yet implemented")
+    }
+
+    override fun readLegacyPermissionStateTEMP() {
+        TODO("Not yet implemented")
+    }
+
+    override fun writeLegacyPermissionStateTEMP() {
+        TODO("Not yet implemented")
+    }
+
+    override fun onSystemReady() {
+        TODO("Not yet implemented")
+    }
+
+    override fun onUserCreated(userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onUserRemoved(userId: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onStorageVolumeMounted(volumeUuid: String, fingerprintChanged: Boolean) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onPackageAdded(
+        androidPackage: AndroidPackage,
+        isInstantApp: Boolean,
+        oldPackage: AndroidPackage?
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onPackageInstalled(
+        androidPackage: AndroidPackage,
+        previousAppId: Int,
+        params: PermissionManagerServiceInternal.PackageInstalledParams,
+        userId: Int
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onPackageUninstalled(
+        packageName: String,
+        appId: Int,
+        androidPackage: AndroidPackage?,
+        sharedUserPkgs: MutableList<AndroidPackage>,
+        userId: Int
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onPackageRemoved(androidPackage: AndroidPackage) {
+        TODO("Not yet implemented")
+    }
+
+    /**
+     * Check whether a UID belongs to an instant app.
+     */
+    private fun PackageManagerLocal.UnfilteredSnapshot.isUidInstantApp(uid: Int): Boolean {
+        if (Process.isIsolatedUid(uid)) {
+            // Unfortunately we don't have the API for getting the owner UID of an isolated UID yet,
+            // so for now we just keep calling the old API.
+            return packageManagerInternal.getInstantAppPackageName(uid) != null
+        }
+        val appId = UserHandle.getAppId(uid)
+        // Instant apps can't have shared UIDs, so we can just take the first package.
+        val firstPackageState = packageStates.values.firstOrNull { it.appId == appId }
+            ?: return false
+        val userId = UserHandle.getUserId(uid)
+        return firstPackageState.getUserStateOrDefault(userId).isInstantApp
+    }
+
+    /**
+     * Check whether a package is visible to a UID within the same user as the UID.
+     */
+    private fun PackageManagerLocal.UnfilteredSnapshot.isPackageVisibleToUid(
+        packageName: String,
+        uid: Int
+    ): Boolean = isPackageVisibleToUid(packageName, UserHandle.getUserId(uid), uid)
+
+    /**
+     * Check whether a package in a particular user is visible to a UID.
+     */
+    private fun PackageManagerLocal.UnfilteredSnapshot.isPackageVisibleToUid(
+        packageName: String,
+        userId: Int,
+        uid: Int
+    ): Boolean {
+        val user = UserHandle.of(userId)
+        return filtered(uid, user).use { it.getPackageState(packageName) != null }
+    }
+}
diff --git a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
index 6479e6a..e081924 100644
--- a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
@@ -18,6 +18,7 @@
 
 import android.Manifest
 import android.content.pm.PackageManager
+import android.content.pm.PermissionGroupInfo
 import android.content.pm.PermissionInfo
 import android.os.Build
 import android.os.UserHandle
@@ -26,6 +27,8 @@
 import com.android.modules.utils.BinaryXmlSerializer
 import com.android.server.permission.access.AccessState
 import com.android.server.permission.access.AccessUri
+import com.android.server.permission.access.GetStateScope
+import com.android.server.permission.access.MutateStateScope
 import com.android.server.permission.access.PermissionUri
 import com.android.server.permission.access.SchemePolicy
 import com.android.server.permission.access.SystemState
@@ -52,19 +55,17 @@
     override val objectScheme: String
         get() = PermissionUri.SCHEME
 
-    override fun getDecision(subject: AccessUri, `object`: AccessUri, state: AccessState): Int {
+    override fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int {
         subject as UidUri
         `object` as PermissionUri
         return state.userStates[subject.userId]?.permissionFlags?.get(subject.appId)
             ?.get(`object`.permissionName) ?: 0
     }
 
-    override fun setDecision(
+    override fun MutateStateScope.setDecision(
         subject: AccessUri,
         `object`: AccessUri,
-        decision: Int,
-        oldState: AccessState,
-        newState: AccessState
+        decision: Int
     ) {
         subject as UidUri
         `object` as PermissionUri
@@ -73,64 +74,58 @@
         uidFlags[`object`.permissionName] = decision
     }
 
-    override fun onUserAdded(userId: Int, oldState: AccessState, newState: AccessState) {
+    override fun MutateStateScope.onUserAdded(userId: Int) {
         newState.systemState.packageStates.forEachValueIndexed { _, packageState ->
-            evaluateAllPermissionStatesForPackageAndUser(
-                packageState, null, userId, oldState, newState
-            )
-            grantImplicitPermissions(packageState, userId, oldState, newState)
+            evaluateAllPermissionStatesForPackageAndUser(packageState, null, userId)
+            grantImplicitPermissions(packageState, userId)
         }
     }
 
-    override fun onAppIdAdded(appId: Int, oldState: AccessState, newState: AccessState) {
+    override fun MutateStateScope.onAppIdAdded(appId: Int) {
         newState.userStates.forEachIndexed { _, _, userState ->
             userState.permissionFlags.getOrPut(appId) { IndexedMap() }
         }
     }
 
-    override fun onAppIdRemoved(appId: Int, oldState: AccessState, newState: AccessState) {
+    override fun MutateStateScope.onAppIdRemoved(appId: Int) {
         newState.userStates.forEachIndexed { _, _, userState -> userState.permissionFlags -= appId }
     }
 
-    override fun onPackageAdded(
-        packageState: PackageState,
-        oldState: AccessState,
-        newState: AccessState
-    ) {
+    override fun MutateStateScope.onPackageAdded(packageState: PackageState) {
         val changedPermissionNames = IndexedSet<String>()
-        adoptPermissions(packageState, changedPermissionNames, newState)
-        addPermissionGroups(packageState, newState)
-        addPermissions(packageState, changedPermissionNames, newState)
+        adoptPermissions(packageState, changedPermissionNames)
+        addPermissionGroups(packageState)
+        addPermissions(packageState, changedPermissionNames)
         // TODO: revokeStoragePermissionsIfScopeExpandedInternal()
-        trimPermissions(packageState.packageName, newState)
-        changedPermissionNames.forEachIndexed { _, it ->
-            evaluatePermissionStateForAllPackages(it, packageState, oldState, newState)
+        trimPermissions(packageState.packageName)
+        changedPermissionNames.forEachIndexed { _, permissionName ->
+            evaluatePermissionStateForAllPackages(permissionName, packageState)
         }
 
-        evaluateAllPermissionStatesForPackage(packageState, packageState, oldState, newState)
-        newState.systemState.userIds.forEachIndexed { _, it ->
-            grantImplicitPermissions(packageState, it, oldState, newState)
+        evaluateAllPermissionStatesForPackage(packageState, packageState)
+        newState.systemState.userIds.forEachIndexed { _, userId ->
+            grantImplicitPermissions(packageState, userId)
         }
 
         // TODO: add trimPermissionStates() here for removing the permission states that are
         // no longer requested. (equivalent to revokeUnusedSharedUserPermissionsLocked())
     }
 
-    private fun adoptPermissions(
+    private fun MutateStateScope.adoptPermissions(
         packageState: PackageState,
-        changedPermissionNames: IndexedSet<String>,
-        newState: AccessState
+        changedPermissionNames: IndexedSet<String>
     ) {
         val `package` = packageState.androidPackage!!
         `package`.adoptPermissions.forEachIndexed { _, originalPackageName ->
             val packageName = `package`.packageName
-            if (!canAdoptPermissions(packageName, originalPackageName, newState)) {
+            if (!canAdoptPermissions(packageName, originalPackageName)) {
                 return@forEachIndexed
             }
             newState.systemState.permissions.let { permissions ->
-                permissions.forEachIndexed { i, permissionName, oldPermission ->
+                permissions.forEachIndexed permissions@ {
+                    permissionIndex, permissionName, oldPermission ->
                     if (oldPermission.packageName != originalPackageName) {
-                        return@forEachIndexed
+                        return@permissions
                     }
                     @Suppress("DEPRECATION")
                     val newPermissionInfo = PermissionInfo().apply {
@@ -140,16 +135,15 @@
                     }
                     val newPermission = Permission(newPermissionInfo, false, oldPermission.type, 0)
                     changedPermissionNames += permissionName
-                    permissions.setValueAt(i, newPermission)
+                    permissions.setValueAt(permissionIndex, newPermission)
                 }
             }
         }
     }
 
-    private fun canAdoptPermissions(
+    private fun MutateStateScope.canAdoptPermissions(
         packageName: String,
-        originalPackageName: String,
-        newState: AccessState
+        originalPackageName: String
     ): Boolean {
         val originalPackageState = newState.systemState.packageStates[originalPackageName]
             ?: return false
@@ -170,7 +164,7 @@
         return true
     }
 
-    private fun addPermissionGroups(packageState: PackageState, newState: AccessState) {
+    private fun MutateStateScope.addPermissionGroups(packageState: PackageState) {
         // Different from the old implementation, which decides whether the app is an instant app by
         // the install flags, now for consistent behavior we allow adding permission groups if the
         // app is non-instant in at least one user.
@@ -202,10 +196,9 @@
         }
     }
 
-    private fun addPermissions(
+    private fun MutateStateScope.addPermissions(
         packageState: PackageState,
-        changedPermissionNames: IndexedSet<String>,
-        newState: AccessState
+        changedPermissionNames: IndexedSet<String>
     ) {
         packageState.androidPackage!!.permissions.forEachIndexed { _, parsedPermission ->
             // TODO:
@@ -229,7 +222,7 @@
             // Different from the old implementation, which may add an (incomplete) signature
             // permission inside another package's permission tree, we now consistently ignore such
             // permissions.
-            val permissionTree = getPermissionTree(permissionName, newState)
+            val permissionTree = getPermissionTree(permissionName)
             val newPackageName = newPermissionInfo.packageName
             if (permissionTree != null && newPackageName != permissionTree.packageName) {
                 Log.w(
@@ -294,10 +287,7 @@
         }
     }
 
-    private fun trimPermissions(
-        packageName: String,
-        newState: AccessState,
-    ) {
+    private fun MutateStateScope.trimPermissions(packageName: String) {
         val packageState = newState.systemState.packageStates[packageName]
         val androidPackage = packageState?.androidPackage
         if (packageState != null && androidPackage == null) {
@@ -314,20 +304,20 @@
         }
 
         newState.systemState.permissions.removeAllIndexed { i, permissionName, permission ->
-            val updatedPermission = updatePermissionIfDynamic(permission, newState)
+            val updatedPermission = updatePermissionIfDynamic(permission)
             newState.systemState.permissions.setValueAt(i, updatedPermission)
             if (updatedPermission.packageName == packageName && (
                 packageState == null || androidPackage!!.permissions.noneIndexed { _, it ->
                     !it.isTree && it.name == permissionName
                 }
             )) {
-                if (!isPermissionDeclaredByDisabledSystemPackage(permission, newState)) {
+                if (!isPermissionDeclaredByDisabledSystemPackage(permission)) {
                     newState.userStates.forEachIndexed { _, userId, userState ->
                         userState.permissionFlags.forEachKeyIndexed { _, appId ->
                             setPermissionFlags(
                                 appId, permissionName, getPermissionFlags(
-                                    appId, permissionName, userId, newState
-                                ) and PermissionFlags.INSTALL_REVOKED, userId, newState
+                                    appId, permissionName, userId
+                                ) and PermissionFlags.INSTALL_REVOKED, userId
                             )
                         }
                     }
@@ -339,9 +329,8 @@
         }
     }
 
-    private fun isPermissionDeclaredByDisabledSystemPackage(
-        permission: Permission,
-        newState: AccessState
+    private fun MutateStateScope.isPermissionDeclaredByDisabledSystemPackage(
+        permission: Permission
     ): Boolean {
         val disabledSystemPackage = newState.systemState
             .disabledSystemPackageStates[permission.packageName]?.androidPackage ?: return false
@@ -350,14 +339,11 @@
         }
     }
 
-    private fun updatePermissionIfDynamic(
-        permission: Permission,
-        newState: AccessState
-    ): Permission {
+    private fun MutateStateScope.updatePermissionIfDynamic(permission: Permission): Permission {
         if (!permission.isDynamic) {
             return permission
         }
-        val permissionTree = getPermissionTree(permission.name, newState) ?: return permission
+        val permissionTree = getPermissionTree(permission.name) ?: return permission
         @Suppress("DEPRECATION")
         return permission.copy(
             permissionInfo = PermissionInfo(permission.permissionInfo).apply {
@@ -366,7 +352,7 @@
         )
     }
 
-    private fun getPermissionTree(permissionName: String, newState: AccessState): Permission? =
+    private fun MutateStateScope.getPermissionTree(permissionName: String): Permission? =
         newState.systemState.permissionTrees.firstNotNullOfOrNullIndexed {
             _, permissionTreeName, permissionTree ->
             if (permissionName.startsWith(permissionTreeName) &&
@@ -378,58 +364,48 @@
             }
         }
 
-    private fun evaluatePermissionStateForAllPackages(
+    private fun MutateStateScope.evaluatePermissionStateForAllPackages(
         permissionName: String,
-        installedPackageState: PackageState?,
-        oldState: AccessState,
-        newState: AccessState
+        installedPackageState: PackageState?
     ) {
         newState.systemState.userIds.forEachIndexed { _, userId ->
             oldState.userStates[userId]?.permissionFlags?.forEachIndexed {
                 _, appId, permissionFlags ->
                 if (permissionName in permissionFlags) {
-                    evaluatePermissionState(
-                        appId, permissionName, installedPackageState, userId, oldState, newState
-                    )
+                    evaluatePermissionState(appId, permissionName, installedPackageState, userId)
                 }
             }
         }
     }
 
-    private fun evaluateAllPermissionStatesForPackage(
+    private fun MutateStateScope.evaluateAllPermissionStatesForPackage(
         packageState: PackageState,
-        installedPackageState: PackageState?,
-        oldState: AccessState,
-        newState: AccessState
+        installedPackageState: PackageState?
     ) {
         newState.systemState.userIds.forEachIndexed { _, userId ->
             evaluateAllPermissionStatesForPackageAndUser(
-                packageState, installedPackageState, userId, oldState, newState
+                packageState, installedPackageState, userId
             )
         }
     }
 
-    private fun evaluateAllPermissionStatesForPackageAndUser(
+    private fun MutateStateScope.evaluateAllPermissionStatesForPackageAndUser(
         packageState: PackageState,
         installedPackageState: PackageState?,
-        userId: Int,
-        oldState: AccessState,
-        newState: AccessState
+        userId: Int
     ) {
-        packageState.androidPackage?.requestedPermissions?.forEachIndexed { _, it ->
+        packageState.androidPackage?.requestedPermissions?.forEachIndexed { _, permissionName ->
             evaluatePermissionState(
-                packageState.appId, it, installedPackageState, userId, oldState, newState
+                packageState.appId, permissionName, installedPackageState, userId
             )
         }
     }
 
-    private fun evaluatePermissionState(
+    private fun MutateStateScope.evaluatePermissionState(
         appId: Int,
         permissionName: String,
         installedPackageState: PackageState?,
-        userId: Int,
-        oldState: AccessState,
-        newState: AccessState
+        userId: Int
     ) {
         val packageNames = newState.systemState.appIds[appId]
         val hasMissingPackage = packageNames.anyIndexed { _, packageName ->
@@ -440,17 +416,17 @@
             return
         }
         val permission = newState.systemState.permissions[permissionName] ?: return
-        val oldFlags = getPermissionFlags(appId, permissionName, userId, newState)
+        val oldFlags = getPermissionFlags(appId, permissionName, userId)
         if (permission.isNormal) {
             val wasGranted = oldFlags.hasBits(PermissionFlags.INSTALL_GRANTED)
             if (!wasGranted) {
                 val wasRevoked = oldFlags.hasBits(PermissionFlags.INSTALL_REVOKED)
                 val isRequestedByInstalledPackage = installedPackageState != null &&
                     permissionName in installedPackageState.androidPackage!!.requestedPermissions
-                val isRequestedBySystemPackage = anyPackageInAppId(appId, newState) {
+                val isRequestedBySystemPackage = anyPackageInAppId(appId) {
                     it.isSystem && permissionName in it.androidPackage!!.requestedPermissions
                 }
-                val isCompatibilityPermission = anyPackageInAppId(appId, newState) {
+                val isCompatibilityPermission = anyPackageInAppId(appId) {
                     isCompatibilityPermissionForPackage(it.androidPackage!!, permissionName)
                 }
                 // If this is an existing, non-system package,
@@ -462,7 +438,7 @@
                 } else {
                     PermissionFlags.INSTALL_REVOKED
                 }
-                setPermissionFlags(appId, permissionName, newFlags, userId, newState)
+                setPermissionFlags(appId, permissionName, newFlags, userId)
             }
         } else if (permission.isSignature || permission.isInternal) {
             val wasProtectionGranted = oldFlags.hasBits(PermissionFlags.PROTECTION_GRANTED)
@@ -471,17 +447,17 @@
                 PermissionFlags.PROTECTION_GRANTED
             } else {
                 val mayGrantByPrivileged = !permission.isPrivileged || (
-                    anyPackageInAppId(appId, newState) {
-                        checkPrivilegedPermissionAllowlist(it, permission, newState)
+                    anyPackageInAppId(appId) {
+                        checkPrivilegedPermissionAllowlist(it, permission)
                     }
                 )
                 val shouldGrantBySignature = permission.isSignature && (
-                    anyPackageInAppId(appId, newState) {
-                        shouldGrantPermissionBySignature(it, permission, newState)
+                    anyPackageInAppId(appId) {
+                        shouldGrantPermissionBySignature(it, permission)
                     }
                 )
-                val shouldGrantByProtectionFlags = anyPackageInAppId(appId, newState) {
-                    shouldGrantPermissionByProtectionFlags(it, permission, newState)
+                val shouldGrantByProtectionFlags = anyPackageInAppId(appId) {
+                    shouldGrantPermissionByProtectionFlags(it, permission)
                 }
                 if (mayGrantByPrivileged &&
                     (shouldGrantBySignature || shouldGrantByProtectionFlags)) {
@@ -501,7 +477,7 @@
             if (permission.isRole) {
                 newFlags = newFlags or (oldFlags and PermissionFlags.ROLE_GRANTED)
             }
-            setPermissionFlags(appId, permissionName, newFlags, userId, newState)
+            setPermissionFlags(appId, permissionName, newFlags, userId)
         } else if (permission.isRuntime) {
             // TODO: add runtime permissions
         } else {
@@ -513,12 +489,7 @@
         // TODO: revokePermissionsNoLongerImplicitLocked() for runtime permissions
     }
 
-    private fun grantImplicitPermissions(
-        packageState: PackageState,
-        userId: Int,
-        oldState: AccessState,
-        newState: AccessState
-    ) {
+    private fun MutateStateScope.grantImplicitPermissions(packageState: PackageState, userId: Int) {
         val appId = packageState.appId
         val androidPackage = packageState.androidPackage ?: return
         androidPackage.implicitPermissions.forEachIndexed implicitPermissions@ {
@@ -530,6 +501,7 @@
             if (!implicitPermission.isRuntime) {
                 return@implicitPermissions
             }
+            // Explicitly check against the old state to determine if this permission is new.
             val isNewPermission = getPermissionFlags(
                 appId, implicitPermissionName, userId, oldState
             ) == 0
@@ -544,7 +516,7 @@
                 checkNotNull(sourcePermission) {
                     "Unknown source permission $sourcePermissionName in split permissions"
                 }
-                val sourceFlags = getPermissionFlags(appId, sourcePermissionName, userId, newState)
+                val sourceFlags = getPermissionFlags(appId, sourcePermissionName, userId)
                 val isSourceGranted = sourceFlags.hasAnyBit(PermissionFlags.MASK_GRANTED)
                 val isNewGranted = newFlags.hasAnyBit(PermissionFlags.MASK_GRANTED)
                 val isGrantingNewFromRevoke = isSourceGranted && !isNewGranted
@@ -559,23 +531,22 @@
                 }
             }
             newFlags = newFlags or PermissionFlags.IMPLICIT
-            setPermissionFlags(appId, implicitPermissionName, newFlags, userId, newState)
+            setPermissionFlags(appId, implicitPermissionName, newFlags, userId)
         }
     }
 
-    private fun getPermissionFlags(
+    private fun MutateStateScope.getPermissionFlags(
         appId: Int,
         permissionName: String,
         userId: Int,
-        state: AccessState
+        state: AccessState = newState
     ): Int = state.userStates[userId].permissionFlags[appId].getWithDefault(permissionName, 0)
 
-    private fun setPermissionFlags(
+    private fun MutateStateScope.setPermissionFlags(
         appId: Int,
         permissionName: String,
         flags: Int,
-        userId: Int,
-        newState: AccessState
+        userId: Int
     ) {
         newState.userStates[userId].permissionFlags[appId]!!
             .putWithDefault(permissionName, flags, 0)
@@ -585,8 +556,9 @@
         androidPackage: AndroidPackage,
         permissionName: String
     ): Boolean {
-        for (info: CompatibilityPermissionInfo in CompatibilityPermissionInfo.COMPAT_PERMS) {
-            if (info.name == permissionName && androidPackage.targetSdkVersion < info.sdkVersion) {
+        for (compatibilityPermission in CompatibilityPermissionInfo.COMPAT_PERMS) {
+            if (compatibilityPermission.name == permissionName &&
+                androidPackage.targetSdkVersion < compatibilityPermission.sdkVersion) {
                 Log.i(
                     LOG_TAG, "Auto-granting $permissionName to old package" +
                     " ${androidPackage.packageName}"
@@ -597,10 +569,9 @@
         return false
     }
 
-    private fun shouldGrantPermissionBySignature(
+    private fun MutateStateScope.shouldGrantPermissionBySignature(
         packageState: PackageState,
-        permission: Permission,
-        newState: AccessState
+        permission: Permission
     ): Boolean {
         // check if the package is allow to use this signature permission.  A package is allowed to
         // use a signature permission if:
@@ -622,10 +593,9 @@
                     SigningDetails.CertCapabilities.PERMISSION)
     }
 
-    private fun checkPrivilegedPermissionAllowlist(
+    private fun MutateStateScope.checkPrivilegedPermissionAllowlist(
         packageState: PackageState,
-        permission: Permission,
-        newState: AccessState
+        permission: Permission
     ): Boolean {
         if (RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_DISABLE) {
             return true
@@ -641,10 +611,10 @@
             newState.systemState.privilegedPermissionAllowlistSourcePackageNames) {
             return true
         }
-        if (isInSystemConfigPrivAppPermissions(androidPackage, permission.name, newState)) {
+        if (isInSystemConfigPrivAppPermissions(androidPackage, permission.name)) {
             return true
         }
-        if (isInSystemConfigPrivAppDenyPermissions(androidPackage, permission.name, newState)) {
+        if (isInSystemConfigPrivAppDenyPermissions(androidPackage, permission.name)) {
             return false
         }
         // Updated system apps do not need to be allowlisted
@@ -655,10 +625,9 @@
         return !RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE
     }
 
-    private fun isInSystemConfigPrivAppPermissions(
+    private fun MutateStateScope.isInSystemConfigPrivAppPermissions(
         androidPackage: AndroidPackage,
-        permissionName: String,
-        newState: AccessState
+        permissionName: String
     ): Boolean {
         val apexModuleName = androidPackage.apexModuleName
         val systemState = newState.systemState
@@ -682,10 +651,9 @@
         return permissionNames?.contains(permissionName) == true
     }
 
-    private fun isInSystemConfigPrivAppDenyPermissions(
+    private fun MutateStateScope.isInSystemConfigPrivAppDenyPermissions(
         androidPackage: AndroidPackage,
-        permissionName: String,
-        newState: AccessState
+        permissionName: String
     ): Boolean {
         // Different from the previous implementation, which may incorrectly use the APEX package
         // name, we now use the APEX module name to be consistent with the allowlist.
@@ -713,22 +681,21 @@
         return permissionNames?.contains(permissionName) == true
     }
 
-    private fun anyPackageInAppId(
+    private fun MutateStateScope.anyPackageInAppId(
         appId: Int,
-        newState: AccessState,
+        state: AccessState = newState,
         predicate: (PackageState) -> Boolean
     ): Boolean {
-        val packageNames = newState.systemState.appIds[appId]
+        val packageNames = state.systemState.appIds[appId]
         return packageNames.anyIndexed { _, packageName ->
-            val packageState = newState.systemState.packageStates[packageName]!!
+            val packageState = state.systemState.packageStates[packageName]!!
             packageState.androidPackage != null && predicate(packageState)
         }
     }
 
-    private fun shouldGrantPermissionByProtectionFlags(
+    private fun MutateStateScope.shouldGrantPermissionByProtectionFlags(
         packageState: PackageState,
-        permission: Permission,
-        newState: AccessState
+        permission: Permission
     ): Boolean {
         val androidPackage = packageState.androidPackage!!
         val knownPackages = newState.systemState.knownPackages
@@ -741,11 +708,9 @@
                     .disabledSystemPackageStates[packageState.packageName]?.androidPackage
                 disabledSystemPackage != null &&
                     permission.name in disabledSystemPackage.requestedPermissions &&
-                    shouldGrantPrivilegedOrOemPermission(
-                        disabledSystemPackage, permission, newState
-                    )
+                    shouldGrantPrivilegedOrOemPermission(disabledSystemPackage, permission)
             } else {
-                shouldGrantPrivilegedOrOemPermission(androidPackage, permission, newState)
+                shouldGrantPrivilegedOrOemPermission(androidPackage, permission)
             }
             if (shouldGrant) {
                 return true
@@ -815,7 +780,7 @@
         }
         if (permission.isRetailDemo &&
             packageName in knownPackages[KnownPackages.PACKAGE_RETAIL_DEMO] &&
-            isDeviceOrProfileOwnerUid(packageState.appId, newState)) {
+            isDeviceOrProfileOwnerUid(packageState.appId)) {
             // Special permission granted only to the OEM specified retail demo app.
             // Note that the original code was passing app ID as UID, so this behavior is kept
             // unchanged.
@@ -829,10 +794,9 @@
         return false
     }
 
-    private fun shouldGrantPrivilegedOrOemPermission(
+    private fun MutateStateScope.shouldGrantPrivilegedOrOemPermission(
         androidPackage: AndroidPackage,
-        permission: Permission,
-        state: AccessState
+        permission: Permission
     ): Boolean {
         val permissionName = permission.name
         val packageName = androidPackage.packageName
@@ -855,7 +819,7 @@
             }
             permission.isOem -> {
                 if (androidPackage.isOem) {
-                    val isOemAllowlisted = state.systemState
+                    val isOemAllowlisted = newState.systemState
                         .oemPermissions[packageName]?.get(permissionName)
                     checkNotNull(isOemAllowlisted) {
                         "OEM permission $permissionName requested by package" +
@@ -868,19 +832,15 @@
         return false
     }
 
-    private fun isDeviceOrProfileOwnerUid(uid: Int, state: AccessState): Boolean {
+    private fun MutateStateScope.isDeviceOrProfileOwnerUid(uid: Int): Boolean {
         val userId = UserHandle.getUserId(uid)
-        val ownerPackageName = state.systemState.deviceAndProfileOwners[userId] ?: return false
-        val ownerPackageState = state.systemState.packageStates[ownerPackageName] ?: return false
+        val ownerPackageName = newState.systemState.deviceAndProfileOwners[userId] ?: return false
+        val ownerPackageState = newState.systemState.packageStates[ownerPackageName] ?: return false
         val ownerUid = UserHandle.getUid(userId, ownerPackageState.appId)
         return uid == ownerUid
     }
 
-    override fun onPackageRemoved(
-        packageState: PackageState,
-        oldState: AccessState,
-        newState: AccessState
-    ) {
+    override fun MutateStateScope.onPackageRemoved(packageState: PackageState) {
         // TODO
     }
 
@@ -892,6 +852,12 @@
         with(persistence) { this@serializeSystemState.serializeSystemState(systemState) }
     }
 
+    fun GetStateScope.getPermissionGroup(permissionGroupName: String): PermissionGroupInfo? =
+        state.systemState.permissionGroups[permissionGroupName]
+
+    fun GetStateScope.getPermission(permissionName: String): Permission? =
+        state.systemState.permissions[permissionName]
+
     companion object {
         private val LOG_TAG = UidPermissionPolicy::class.java.simpleName
 
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OverlayPathsUninstallSystemUpdatesTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OverlayPathsUninstallSystemUpdatesTest.kt
new file mode 100644
index 0000000..4044780
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OverlayPathsUninstallSystemUpdatesTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm.test
+
+import com.android.internal.util.test.SystemPreparer
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+
+@RunWith(DeviceJUnit4ClassRunner::class)
+class OverlayPathsUninstallSystemUpdatesTest : BaseHostJUnit4Test() {
+
+    companion object {
+        private const val TEST_PKG_NAME = "com.android.server.pm.test.test_app"
+        private const val VERSION_ONE = "PackageManagerTestAppVersion1.apk"
+        private const val VERSION_TWO = "PackageManagerTestAppVersion2.apk"
+
+        @get:ClassRule
+        val deviceRebootRule = SystemPreparer.TestRuleDelegate(true)
+    }
+
+    private val tempFolder = TemporaryFolder()
+    private val preparer: SystemPreparer = SystemPreparer(tempFolder,
+            SystemPreparer.RebootStrategy.FULL, deviceRebootRule, true) { this.device }
+
+    @Rule
+    @JvmField
+    val rules = RuleChain.outerRule(tempFolder).around(preparer)!!
+    private val filePath = HostUtils.makePathForApk("PackageManagerTestApp.apk", Partition.PRODUCT)
+
+    @Before
+    @After
+    fun removeApk() {
+        device.uninstallPackage(TEST_PKG_NAME)
+    }
+
+    @Test
+    fun verify() {
+        // First, push a system app to the device and then update it so there's a data variant
+        preparer.pushResourceFile(VERSION_ONE, filePath.toString())
+                .reboot()
+
+        val versionTwoFile = HostUtils.copyResourceToHostFile(VERSION_TWO, tempFolder.newFile())
+
+        assertThat(device.installPackage(versionTwoFile, true)).isNull()
+
+        device.executeShellCommand(
+                "cmd overlay fabricate --target-name TestResources" +
+                " --target $TEST_PKG_NAME" +
+                " --name UninstallSystemUpdatesTest" +
+                " $TEST_PKG_NAME:color/overlay_test 0x1C 0xFFFFFFFF"
+        )
+
+        device.executeShellCommand(
+                "cmd overlay enable --user 0 com.android.shell:UninstallSystemUpdatesTest"
+        )
+
+        fun verifyValueOverlaid() {
+            assertThat(device.executeShellCommand(
+                    "cmd overlay lookup --user 0 $TEST_PKG_NAME $TEST_PKG_NAME:color/overlay_test"
+            ).trim()).isEqualTo("#ffffffff")
+        }
+
+        verifyValueOverlaid()
+
+        assertThat(
+                device.executeShellCommand("pm uninstall-system-updates $TEST_PKG_NAME"
+        ).trim()).endsWith("Success")
+
+        // Wait for paths to re-propagate. This doesn't do a retry loop in case the path clear also
+        // has some latency. There must be some minimum wait time for the paths to settle, and then
+        // a wait time for the paths to re-propagate. Rather than complicate the logic, just wait
+        // a long enough time for both events to occur.
+        Thread.sleep(5000)
+
+        verifyValueOverlaid()
+    }
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Generic/res/values/colors.xml b/services/tests/PackageManagerServiceTests/host/test-apps/Generic/res/values/colors.xml
new file mode 100644
index 0000000..5a41d88
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Generic/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources>
+    <color name="overlay_test">#FF000000</color>
+</resources>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Generic/res/values/overlayable.xml b/services/tests/PackageManagerServiceTests/host/test-apps/Generic/res/values/overlayable.xml
new file mode 100644
index 0000000..c5ba450
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Generic/res/values/overlayable.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources>
+    <overlayable name="TestResources">
+        <policy type="public">
+            <item type="color" name="overlay_test" />
+        </policy>
+    </overlayable>
+</resources>
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt
index faa2352..5f26d6f 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt
@@ -53,6 +53,11 @@
         snapshot.use {
             val packageStates = it.packageStates
 
+            // Check for unmodifiable
+            assertFailsWith(UnsupportedOperationException::class) {
+                it.packageStates.clear()
+            }
+
             // Check contents
             assertThat(packageStates).containsExactly(
                 packageStateAll.packageName, packageStateAll,
@@ -78,9 +83,14 @@
             assertThat(filteredOne.getPackageState(packageStateUser10.packageName)).isNull()
 
             filteredThree.use {
-                val statesList = mutableListOf<PackageState>()
-                assertThat(it.forAllPackageStates { statesList += it })
-                assertThat(statesList).containsExactly(packageStateAll, packageStateUser10)
+                // Check for unmodifiable
+                assertFailsWith(UnsupportedOperationException::class) {
+                    it.packageStates.clear()
+                }
+                assertThat(it.packageStates).containsExactly(
+                    packageStateAll.packageName, packageStateAll,
+                    packageStateUser10.packageName, packageStateUser10,
+                )
             }
 
             // Call after child close, parent open fails
@@ -96,7 +106,7 @@
 
         // Call after close fails
         assertClosedFailure { snapshot.packageStates }
-        assertClosedFailure { filteredOne.forAllPackageStates {} }
+        assertClosedFailure { filteredOne.packageStates }
         assertClosedFailure {
             filteredTwo.getPackageState(packageStateAll.packageName)
         }
@@ -116,9 +126,15 @@
                 .isEqualTo(packageStateUser0)
             assertThat(it.getPackageState(packageStateUser10.packageName)).isNull()
 
-            val statesList = mutableListOf<PackageState>()
-            assertThat(it.forAllPackageStates { statesList += it })
-            assertThat(statesList).containsExactly(packageStateAll, packageStateUser0)
+            // Check for unmodifiable
+            assertFailsWith(UnsupportedOperationException::class) {
+                it.packageStates.clear()
+            }
+
+            assertThat(it.packageStates).containsExactly(
+                packageStateAll.packageName, packageStateAll,
+                packageStateUser0.packageName, packageStateUser0,
+            )
         }
 
         // Call after close fails
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index c87fd26..de09b19 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -592,6 +592,73 @@
     }
 
     /**
+     * Verify that offload broadcasts are not starved because of broadcasts in higher priority
+     * queues.
+     */
+    @Test
+    public void testOffloadStarvation() {
+        final BroadcastOptions optInteractive = BroadcastOptions.makeBasic();
+        optInteractive.setInteractive(true);
+
+        mConstants.MAX_CONSECUTIVE_URGENT_DISPATCHES = 1;
+        mConstants.MAX_CONSECUTIVE_NORMAL_DISPATCHES = 2;
+        final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
+                PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+
+        // mix of broadcasts, with more than 2 normal
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_BOOT_COMPLETED)
+                        .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_PACKAGE_CHANGED)
+                        .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_APPLICATION_PREFERENCES),
+                        optInteractive), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE),
+                        optInteractive), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_INPUT_METHOD_CHANGED)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_NEW_OUTGOING_CALL),
+                        optInteractive), 0);
+
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_LOCALE_CHANGED, queue.getActive().intent.getAction());
+        // after MAX_CONSECUTIVE_URGENT_DISPATCHES expect an ordinary one next
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_TIMEZONE_CHANGED, queue.getActive().intent.getAction());
+        // and then back to prioritizing urgent ones
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_APPLICATION_PREFERENCES, queue.getActive().intent.getAction());
+        // after MAX_CONSECUTIVE_URGENT_DISPATCHES, again an ordinary one next
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_ALARM_CHANGED, queue.getActive().intent.getAction());
+        // and then back to prioritizing urgent ones
+        queue.makeActiveNextPending();
+        assertEquals(AppWidgetManager.ACTION_APPWIDGET_UPDATE,
+                queue.getActive().intent.getAction());
+        // after MAX_CONSECUTIVE_URGENT_DISPATCHES and MAX_CONSECUTIVE_NORMAL_DISPATCHES,
+        // expect an offload one
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_BOOT_COMPLETED, queue.getActive().intent.getAction());
+        // and then back to prioritizing urgent ones
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_INPUT_METHOD_CHANGED, queue.getActive().intent.getAction());
+    }
+
+    /**
      * Verify that sending a broadcast that removes any matching pending
      * broadcasts is applied as expected.
      */
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
index be32b79..f105971 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -212,7 +212,7 @@
         SurfaceControl.DisplayMode displayMode = createFakeDisplayMode(0, 1920, 1080, 0);
         displayMode.supportedHdrTypes = new int[0];
         FakeDisplay display = new FakeDisplay(PORT_A, new SurfaceControl.DisplayMode[]{displayMode},
-                0);
+                0, 0);
         setUpDisplay(display);
         updateAvailableDisplays();
         mAdapter.registerLocked();
@@ -393,7 +393,7 @@
         SurfaceControl.DisplayMode displayMode = createFakeDisplayMode(0, 1920, 1080, 60f);
         SurfaceControl.DisplayMode[] modes =
                 new SurfaceControl.DisplayMode[]{displayMode};
-        FakeDisplay display = new FakeDisplay(PORT_A, modes, 0);
+        FakeDisplay display = new FakeDisplay(PORT_A, modes, 0, displayMode.refreshRate);
         setUpDisplay(display);
         updateAvailableDisplays();
         mAdapter.registerLocked();
@@ -449,7 +449,7 @@
         SurfaceControl.DisplayMode displayMode = createFakeDisplayMode(0, 1920, 1080, 60f);
         SurfaceControl.DisplayMode[] modes =
                 new SurfaceControl.DisplayMode[]{displayMode};
-        FakeDisplay display = new FakeDisplay(PORT_A, modes, 0);
+        FakeDisplay display = new FakeDisplay(PORT_A, modes, 0, displayMode.refreshRate);
         setUpDisplay(display);
         updateAvailableDisplays();
         mAdapter.registerLocked();
@@ -504,7 +504,7 @@
                 createFakeDisplayMode(0, 1920, 1080, 60f),
                 createFakeDisplayMode(1, 1920, 1080, 50f)
         };
-        FakeDisplay display = new FakeDisplay(PORT_A, modes, /* activeMode */ 0);
+        FakeDisplay display = new FakeDisplay(PORT_A, modes, /* activeMode */ 0, 60f);
         setUpDisplay(display);
         updateAvailableDisplays();
         mAdapter.registerLocked();
@@ -538,6 +538,49 @@
     }
 
     @Test
+    public void testAfterDisplayChange_RenderFrameRateIsUpdated() throws Exception {
+        SurfaceControl.DisplayMode[] modes = new SurfaceControl.DisplayMode[]{
+                createFakeDisplayMode(0, 1920, 1080, 60f),
+        };
+        FakeDisplay display = new FakeDisplay(PORT_A, modes, /* activeMode */ 0,
+                /* renderFrameRate */30f);
+        setUpDisplay(display);
+        updateAvailableDisplays();
+        mAdapter.registerLocked();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        assertThat(mListener.changedDisplays).isEmpty();
+
+        DisplayDeviceInfo displayDeviceInfo = mListener.addedDisplays.get(0)
+                .getDisplayDeviceInfoLocked();
+
+        Display.Mode activeMode = getModeById(displayDeviceInfo, displayDeviceInfo.modeId);
+        assertThat(activeMode.matches(1920, 1080, 60f)).isTrue();
+        assertEquals(Float.floatToIntBits(30f),
+                Float.floatToIntBits(displayDeviceInfo.renderFrameRate));
+
+        // Change the render frame rate
+        display.dynamicInfo.renderFrameRate = 60f;
+        setUpDisplay(display);
+        mInjector.getTransmitter().sendHotplug(display, /* connected */ true);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+        assertTrue(mListener.traversalRequested);
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        assertThat(mListener.changedDisplays.size()).isEqualTo(1);
+
+        DisplayDevice displayDevice = mListener.changedDisplays.get(0);
+        displayDevice.applyPendingDisplayDeviceInfoChangesLocked();
+        displayDeviceInfo = displayDevice.getDisplayDeviceInfoLocked();
+
+        activeMode = getModeById(displayDeviceInfo, displayDeviceInfo.modeId);
+        assertThat(activeMode.matches(1920, 1080, 60f)).isTrue();
+        assertEquals(Float.floatToIntBits(60f),
+                Float.floatToIntBits(displayDeviceInfo.renderFrameRate));
+    }
+
+    @Test
     public void testAfterDisplayChange_HdrCapabilitiesAreUpdated() throws Exception {
         FakeDisplay display = new FakeDisplay(PORT_A);
         Display.HdrCapabilities initialHdrCapabilities = new Display.HdrCapabilities(new int[0],
@@ -722,7 +765,7 @@
                 createFakeDisplayMode(1, 1920, 1080, 50f)
         };
         final int activeMode = 0;
-        FakeDisplay display = new FakeDisplay(PORT_A, modes, activeMode);
+        FakeDisplay display = new FakeDisplay(PORT_A, modes, activeMode, 60f);
         display.desiredDisplayModeSpecs.defaultMode = 1;
 
         setUpDisplay(display);
@@ -979,11 +1022,13 @@
             dynamicInfo.activeDisplayModeId = 0;
         }
 
-        private FakeDisplay(int port, SurfaceControl.DisplayMode[] modes, int activeMode) {
+        private FakeDisplay(int port, SurfaceControl.DisplayMode[] modes, int activeMode,
+                float renderFrameRate) {
             address = createDisplayAddress(port);
             info = createFakeDisplayInfo();
             dynamicInfo.supportedDisplayModes = modes;
             dynamicInfo.activeDisplayModeId = activeMode;
+            dynamicInfo.renderFrameRate = renderFrameRate;
         }
 
         private FakeDisplay(int port, SurfaceControl.DisplayMode[] modes, int activeMode,
diff --git a/services/tests/servicestests/res/xml/power_profile_test_legacy_modem.xml b/services/tests/servicestests/res/xml/power_profile_test_legacy_modem.xml
new file mode 100644
index 0000000..5335f96
--- /dev/null
+++ b/services/tests/servicestests/res/xml/power_profile_test_legacy_modem.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<device name="Android">
+    <!-- Cellular modem related values. These constants are deprecated, but still supported and
+         need to be tested -->
+    <item name="radio.scanning">720</item>
+    <item name="modem.controller.sleep">70</item>
+    <item name="modem.controller.idle">360</item>
+    <item name="modem.controller.rx">1440</item>
+    <array name="modem.controller.tx"> <!-- Strength 0 to 4 -->
+        <value>720</value>
+        <value>1080</value>
+        <value>1440</value>
+        <value>1800</value>
+        <value>2160</value>
+    </array>
+</device>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/xml/power_profile_test_modem_calculator.xml b/services/tests/servicestests/res/xml/power_profile_test_modem_calculator.xml
new file mode 100644
index 0000000..f57bc0f
--- /dev/null
+++ b/services/tests/servicestests/res/xml/power_profile_test_modem_calculator.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<device name="Android">
+    <modem>
+        <sleep>70</sleep>
+        <idle>360</idle>
+        <active rat="DEFAULT">
+            <receive>1440</receive>
+            <transmit level="0">720</transmit>
+            <transmit level="1">1080</transmit>
+            <transmit level="2">1440</transmit>
+            <transmit level="3">1800</transmit>
+            <transmit level="4">2160</transmit>
+        </active>
+    </modem>
+</device>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/xml/power_profile_test_modem_calculator_multiactive.xml b/services/tests/servicestests/res/xml/power_profile_test_modem_calculator_multiactive.xml
new file mode 100644
index 0000000..4f5e674
--- /dev/null
+++ b/services/tests/servicestests/res/xml/power_profile_test_modem_calculator_multiactive.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<device name="Android">
+    <modem>
+        <sleep>70</sleep>
+        <idle>360</idle>
+        <active rat="DEFAULT">
+            <receive>1440</receive>
+            <transmit level="0">720</transmit>
+            <transmit level="1">1080</transmit>
+            <transmit level="2">1440</transmit>
+            <transmit level="3">1800</transmit>
+            <transmit level="4">2160</transmit>
+        </active>
+        <active rat="LTE">
+            <receive>2000</receive>
+            <transmit level="0">800</transmit>
+            <transmit level="1">1200</transmit>
+            <transmit level="2">1600</transmit>
+            <transmit level="3">2000</transmit>
+            <transmit level="4">2400</transmit>
+        </active>
+        <active rat="NR" nrFrequency="DEFAULT">
+            <receive>2222</receive>
+            <transmit level="0">999</transmit>
+            <transmit level="1">1333</transmit>
+            <transmit level="2">1888</transmit>
+            <transmit level="3">2222</transmit>
+            <transmit level="4">2666</transmit>
+        </active>
+        <active rat="NR" nrFrequency="HIGH">
+            <receive>2727</receive>
+            <transmit level="0">1818</transmit>
+            <transmit level="1">2727</transmit>
+            <transmit level="2">3636</transmit>
+            <transmit level="3">4545</transmit>
+            <transmit level="4">5454</transmit>
+        </active>
+        <active rat="NR" nrFrequency="MMWAVE">
+            <receive>3456</receive>
+            <transmit level="0">2345</transmit>
+            <transmit level="1">3456</transmit>
+            <transmit level="2">4567</transmit>
+            <transmit level="3">5678</transmit>
+            <transmit level="4">6789</transmit>
+        </active>
+    </modem>
+</device>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/xml/power_profile_test_modem_default.xml b/services/tests/servicestests/res/xml/power_profile_test_modem_default.xml
new file mode 100644
index 0000000..ab016fb
--- /dev/null
+++ b/services/tests/servicestests/res/xml/power_profile_test_modem_default.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<device name="Android">
+    <modem>
+        <!-- Modem sleep drain current value in mA. -->
+        <sleep>10</sleep>
+        <!-- Modem idle drain current value in mA. -->
+        <idle>20</idle>
+        <active rat="DEFAULT">
+            <!-- Transmit current drain in mA. -->
+            <receive>30</receive>
+            <!-- Transmit current drains in mA. Must be defined for all levels (0 to 4) -->
+            <transmit level="0">40</transmit>
+            <transmit level="1">50</transmit>
+            <transmit level="2">60</transmit>
+            <transmit level="3">70</transmit>
+            <transmit level="4">80</transmit>
+        </active>
+    </modem>
+</device>
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
new file mode 100644
index 0000000..cbe6d26
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2012 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;
+
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.util.DebugUtils.valueToString;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.INetdUnsolicitedEventListener;
+import android.net.LinkAddress;
+import android.net.NetworkPolicyManager;
+import android.os.BatteryStats;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArrayMap;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.IBatteryStats;
+import com.android.server.NetworkManagementService.Dependencies;
+import com.android.server.net.BaseNetworkObserver;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.BiFunction;
+
+/**
+ * Tests for {@link NetworkManagementService}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkManagementServiceTest {
+    private NetworkManagementService mNMService;
+    @Mock private Context mContext;
+    @Mock private ConnectivityManager mCm;
+    @Mock private IBatteryStats.Stub mBatteryStatsService;
+    @Mock private INetd.Stub mNetdService;
+
+    private static final int TEST_UID = 111;
+
+    @NonNull
+    @Captor
+    private ArgumentCaptor<INetdUnsolicitedEventListener> mUnsolListenerCaptor;
+
+    private final MockDependencies mDeps = new MockDependencies();
+
+    private final class MockDependencies extends Dependencies {
+        @Override
+        public IBinder getService(String name) {
+            switch (name) {
+                case BatteryStats.SERVICE_NAME:
+                    return mBatteryStatsService;
+                default:
+                    throw new UnsupportedOperationException("Unknown service " + name);
+            }
+        }
+
+        @Override
+        public void registerLocalService(NetworkManagementInternal nmi) {
+        }
+
+        @Override
+        public INetd getNetd() {
+            return mNetdService;
+        }
+
+        @Override
+        public int getCallingUid() {
+            return Process.SYSTEM_UID;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doNothing().when(mNetdService)
+                .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture());
+        doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+                eq(ConnectivityManager.class));
+        doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
+        // Start the service and wait until it connects to our socket.
+        mNMService = NetworkManagementService.create(mContext, mDeps);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mNMService.shutdown();
+    }
+
+    private static <T> T expectSoon(T mock) {
+        return verify(mock, timeout(200));
+    }
+
+    /**
+     * Tests that network observers work properly.
+     */
+    @Test
+    public void testNetworkObservers() throws Exception {
+        BaseNetworkObserver observer = mock(BaseNetworkObserver.class);
+        doReturn(new Binder()).when(observer).asBinder();  // Used by registerObserver.
+        mNMService.registerObserver(observer);
+
+        // Forget everything that happened to the mock so far, so we can explicitly verify
+        // everything that happens and does not happen to it from now on.
+
+        INetdUnsolicitedEventListener unsolListener = mUnsolListenerCaptor.getValue();
+        reset(observer);
+        // Now call unsolListener methods and ensure that the observer methods are
+        // called. After every method we expect a callback soon after; to ensure that
+        // invalid messages don't cause any callbacks, we call verifyNoMoreInteractions at the end.
+
+        /**
+         * Interface changes.
+         */
+        unsolListener.onInterfaceAdded("rmnet12");
+        expectSoon(observer).interfaceAdded("rmnet12");
+
+        unsolListener.onInterfaceRemoved("eth1");
+        expectSoon(observer).interfaceRemoved("eth1");
+
+        unsolListener.onInterfaceChanged("clat4", true);
+        expectSoon(observer).interfaceStatusChanged("clat4", true);
+
+        unsolListener.onInterfaceLinkStateChanged("rmnet0", false);
+        expectSoon(observer).interfaceLinkStateChanged("rmnet0", false);
+
+        /**
+         * Bandwidth control events.
+         */
+        unsolListener.onQuotaLimitReached("data", "rmnet_usb0");
+        expectSoon(observer).limitReached("data", "rmnet_usb0");
+
+        /**
+         * Interface class activity.
+         */
+        unsolListener.onInterfaceClassActivityChanged(true, 1, 1234, TEST_UID);
+        expectSoon(observer).interfaceClassDataActivityChanged(1, true, 1234, TEST_UID);
+
+        unsolListener.onInterfaceClassActivityChanged(false, 9, 5678, TEST_UID);
+        expectSoon(observer).interfaceClassDataActivityChanged(9, false, 5678, TEST_UID);
+
+        unsolListener.onInterfaceClassActivityChanged(false, 9, 4321, TEST_UID);
+        expectSoon(observer).interfaceClassDataActivityChanged(9, false, 4321, TEST_UID);
+
+        /**
+         * IP address changes.
+         */
+        unsolListener.onInterfaceAddressUpdated("fe80::1/64", "wlan0", 128, 253);
+        expectSoon(observer).addressUpdated("wlan0", new LinkAddress("fe80::1/64", 128, 253));
+
+        unsolListener.onInterfaceAddressRemoved("fe80::1/64", "wlan0", 128, 253);
+        expectSoon(observer).addressRemoved("wlan0", new LinkAddress("fe80::1/64", 128, 253));
+
+        unsolListener.onInterfaceAddressRemoved("2001:db8::1/64", "wlan0", 1, 0);
+        expectSoon(observer).addressRemoved("wlan0", new LinkAddress("2001:db8::1/64", 1, 0));
+
+        /**
+         * DNS information broadcasts.
+         */
+        unsolListener.onInterfaceDnsServerInfo("rmnet_usb0", 3600, new String[]{"2001:db8::1"});
+        expectSoon(observer).interfaceDnsServerInfo("rmnet_usb0", 3600,
+                new String[]{"2001:db8::1"});
+
+        unsolListener.onInterfaceDnsServerInfo("wlan0", 14400,
+                new String[]{"2001:db8::1", "2001:db8::2"});
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", 14400,
+                new String[]{"2001:db8::1", "2001:db8::2"});
+
+        // We don't check for negative lifetimes, only for parse errors.
+        unsolListener.onInterfaceDnsServerInfo("wlan0", -3600, new String[]{"::1"});
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", -3600,
+                new String[]{"::1"});
+
+        // No syntax checking on the addresses.
+        unsolListener.onInterfaceDnsServerInfo("wlan0", 600,
+                new String[]{"", "::", "", "foo", "::1"});
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", 600,
+                new String[]{"", "::", "", "foo", "::1"});
+
+        // Make sure nothing else was called.
+        verifyNoMoreInteractions(observer);
+    }
+
+    @Test
+    public void testFirewallEnabled() {
+        mNMService.setFirewallEnabled(true);
+        assertTrue(mNMService.isFirewallEnabled());
+
+        mNMService.setFirewallEnabled(false);
+        assertFalse(mNMService.isFirewallEnabled());
+    }
+
+    @Test
+    public void testNetworkRestrictedDefault() {
+        assertFalse(mNMService.isNetworkRestricted(TEST_UID));
+    }
+
+    @Test
+    public void testMeteredNetworkRestrictions() throws RemoteException {
+        // Make sure the mocked netd method returns true.
+        doReturn(true).when(mNetdService).bandwidthEnableDataSaver(anyBoolean());
+
+        // Restrict usage of mobile data in background
+        mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true);
+        assertTrue("Should be true since mobile data usage is restricted",
+                mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).addUidToMeteredNetworkDenyList(TEST_UID);
+
+        mNMService.setDataSaverModeEnabled(true);
+        verify(mNetdService).bandwidthEnableDataSaver(true);
+
+        mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false);
+        assertTrue("Should be true since data saver is on and the uid is not allowlisted",
+                mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).removeUidFromMeteredNetworkDenyList(TEST_UID);
+
+        mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true);
+        assertFalse("Should be false since data saver is on and the uid is allowlisted",
+                mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).addUidToMeteredNetworkAllowList(TEST_UID);
+
+        // remove uid from allowlist and turn datasaver off again
+        mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
+        verify(mCm).removeUidFromMeteredNetworkAllowList(TEST_UID);
+        mNMService.setDataSaverModeEnabled(false);
+        verify(mNetdService).bandwidthEnableDataSaver(false);
+        assertFalse("Network should not be restricted when data saver is off",
+                mNMService.isNetworkRestricted(TEST_UID));
+    }
+
+    @Test
+    public void testFirewallChains() {
+        final ArrayMap<Integer, ArrayMap<Integer, Boolean>> expected = new ArrayMap<>();
+        // Dozable chain
+        final ArrayMap<Integer, Boolean> isRestrictedForDozable = new ArrayMap<>();
+        isRestrictedForDozable.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+        isRestrictedForDozable.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForDozable.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(FIREWALL_CHAIN_DOZABLE, isRestrictedForDozable);
+        // Powersaver chain
+        final ArrayMap<Integer, Boolean> isRestrictedForPowerSave = new ArrayMap<>();
+        isRestrictedForPowerSave.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+        isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(FIREWALL_CHAIN_POWERSAVE, isRestrictedForPowerSave);
+        // Standby chain
+        final ArrayMap<Integer, Boolean> isRestrictedForStandby = new ArrayMap<>();
+        isRestrictedForStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, false);
+        isRestrictedForStandby.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForStandby.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(FIREWALL_CHAIN_STANDBY, isRestrictedForStandby);
+        // Restricted mode chain
+        final ArrayMap<Integer, Boolean> isRestrictedForRestrictedMode = new ArrayMap<>();
+        isRestrictedForRestrictedMode.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+        isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(FIREWALL_CHAIN_RESTRICTED, isRestrictedForRestrictedMode);
+        // Low Power Standby chain
+        final ArrayMap<Integer, Boolean> isRestrictedForLowPowerStandby = new ArrayMap<>();
+        isRestrictedForLowPowerStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+        isRestrictedForLowPowerStandby.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForLowPowerStandby.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, isRestrictedForLowPowerStandby);
+
+        final int[] chains = {
+                FIREWALL_CHAIN_STANDBY,
+                FIREWALL_CHAIN_POWERSAVE,
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_RESTRICTED,
+                FIREWALL_CHAIN_LOW_POWER_STANDBY
+        };
+        final int[] states = {
+                INetd.FIREWALL_RULE_ALLOW,
+                INetd.FIREWALL_RULE_DENY,
+                NetworkPolicyManager.FIREWALL_RULE_DEFAULT
+        };
+        BiFunction<Integer, Integer, String> errorMsg = (chain, state) -> {
+            return String.format("Unexpected value for chain: %s and state: %s",
+                    valueToString(INetd.class, "FIREWALL_CHAIN_", chain),
+                    valueToString(INetd.class, "FIREWALL_RULE_", state));
+        };
+        for (int chain : chains) {
+            final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain);
+            mNMService.setFirewallChainEnabled(chain, true);
+            verify(mCm).setFirewallChainEnabled(chain, true /* enabled */);
+            for (int state : states) {
+                mNMService.setFirewallUidRule(chain, TEST_UID, state);
+                assertEquals(errorMsg.apply(chain, state),
+                        expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID));
+            }
+            mNMService.setFirewallChainEnabled(chain, false);
+            verify(mCm).setFirewallChainEnabled(chain, false /* enabled */);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 6e446f0..d47f063 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -638,6 +638,39 @@
                 /* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
     }
 
+    @Test
+    public void testStopUser_invalidUser() {
+        int userId = -1;
+
+        assertThrows(IllegalArgumentException.class,
+                () -> mUserController.stopUser(userId, /* force= */ true,
+                        /* allowDelayedLocking= */ true, /* stopUserCallback= */ null,
+                        /* keyEvictedCallback= */ null));
+    }
+
+    @Test
+    public void testStopUser_systemUser() {
+        int userId = UserHandle.USER_SYSTEM;
+
+        int r = mUserController.stopUser(userId, /* force= */ true,
+                /* allowDelayedLocking= */ true, /* stopUserCallback= */ null,
+                /* keyEvictedCallback= */ null);
+
+        assertThat(r).isEqualTo(ActivityManager.USER_OP_ERROR_IS_SYSTEM);
+    }
+
+    @Test
+    public void testStopUser_currentUser() {
+        setUpUser(TEST_USER_ID1, /* flags= */ 0);
+        mUserController.startUser(TEST_USER_ID1, /* foreground= */ true);
+
+        int r = mUserController.stopUser(TEST_USER_ID1, /* force= */ true,
+                /* allowDelayedLocking= */ true, /* stopUserCallback= */ null,
+                /* keyEvictedCallback= */ null);
+
+        assertThat(r).isEqualTo(ActivityManager.USER_OP_IS_CURRENT);
+    }
+
     /**
      * Test conditional delayed locking with mDelayUserDataLocking true.
      */
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 3daf0f8..9de6190 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -58,15 +58,18 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
-import android.graphics.Point;
 import android.hardware.Sensor;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.IInputManager;
+import android.hardware.input.VirtualDpadConfig;
 import android.hardware.input.VirtualKeyEvent;
+import android.hardware.input.VirtualKeyboardConfig;
 import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
 import android.hardware.input.VirtualTouchEvent;
+import android.hardware.input.VirtualTouchscreenConfig;
 import android.net.MacAddress;
 import android.os.Binder;
 import android.os.Handler;
@@ -135,7 +138,37 @@
     private static final int SENSOR_HANDLE = 64;
     private static final Binder BINDER = new Binder("binder");
     private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
-    private static final int VIRTUAL_DEVICE_ID =  42;
+    private static final int VIRTUAL_DEVICE_ID = 42;
+    private static final VirtualDpadConfig DPAD_CONFIG =
+            new VirtualDpadConfig.Builder()
+                    .setVendorId(VENDOR_ID)
+                    .setProductId(PRODUCT_ID)
+                    .setInputDeviceName(DEVICE_NAME)
+                    .setAssociatedDisplayId(DISPLAY_ID)
+                    .build();
+    private static final VirtualKeyboardConfig KEYBOARD_CONFIG =
+            new VirtualKeyboardConfig.Builder()
+                    .setVendorId(VENDOR_ID)
+                    .setProductId(PRODUCT_ID)
+                    .setInputDeviceName(DEVICE_NAME)
+                    .setAssociatedDisplayId(DISPLAY_ID)
+                    .build();
+    private static final VirtualMouseConfig MOUSE_CONFIG =
+            new VirtualMouseConfig.Builder()
+                    .setVendorId(VENDOR_ID)
+                    .setProductId(PRODUCT_ID)
+                    .setInputDeviceName(DEVICE_NAME)
+                    .setAssociatedDisplayId(DISPLAY_ID)
+                    .build();
+    private static final VirtualTouchscreenConfig TOUCHSCREEN_CONFIG =
+            new VirtualTouchscreenConfig.Builder()
+                    .setVendorId(VENDOR_ID)
+                    .setProductId(PRODUCT_ID)
+                    .setInputDeviceName(DEVICE_NAME)
+                    .setAssociatedDisplayId(DISPLAY_ID)
+                    .setWidthInPixels(WIDTH)
+                    .setHeightInPixels(HEIGHT)
+                    .build();
 
     private Context mContext;
     private InputManagerMockHelper mInputManagerMockHelper;
@@ -477,63 +510,77 @@
 
     @Test
     public void createVirtualDpad_noDisplay_failsSecurityException() {
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualDpad(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
-                        PRODUCT_ID, BINDER));
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
     }
 
     @Test
     public void createVirtualKeyboard_noDisplay_failsSecurityException() {
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
-                        PRODUCT_ID, BINDER));
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
     }
 
     @Test
     public void createVirtualMouse_noDisplay_failsSecurityException() {
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
-                        PRODUCT_ID, BINDER));
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
     }
 
     @Test
     public void createVirtualTouchscreen_noDisplay_failsSecurityException() {
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME,
-                        VENDOR_ID, PRODUCT_ID, BINDER, new Point(WIDTH, HEIGHT)));
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
     }
 
     @Test
     public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
-        Point size = new Point(0, 0);
+        final VirtualTouchscreenConfig zeroConfig =
+                new VirtualTouchscreenConfig.Builder()
+                        .setVendorId(VENDOR_ID)
+                        .setProductId(PRODUCT_ID)
+                        .setInputDeviceName(DEVICE_NAME)
+                        .setAssociatedDisplayId(DISPLAY_ID)
+                        .setWidthInPixels(0)
+                        .setHeightInPixels(0)
+                        .build();
         assertThrows(IllegalArgumentException.class,
-                () -> mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
-                        PRODUCT_ID, BINDER, size));
+                () -> mDeviceImpl.createVirtualTouchscreen(zeroConfig, BINDER));
     }
 
     @Test
     public void createVirtualTouchscreen_negativeDisplayDimension_failsIllegalArgumentException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
-        Point size = new Point(-100, -100);
+        final VirtualTouchscreenConfig negativeConfig =
+                new VirtualTouchscreenConfig.Builder()
+                        .setVendorId(VENDOR_ID)
+                        .setProductId(PRODUCT_ID)
+                        .setInputDeviceName(DEVICE_NAME)
+                        .setAssociatedDisplayId(DISPLAY_ID)
+                        .setWidthInPixels(-100)
+                        .setHeightInPixels(-100)
+                        .build();
         assertThrows(IllegalArgumentException.class,
-                () -> mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
-                        PRODUCT_ID, BINDER, size));
+                () -> mDeviceImpl.createVirtualTouchscreen(negativeConfig, BINDER));
+
     }
 
     @Test
     public void createVirtualTouchscreen_positiveDisplayDimension_successful() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
-        Point size = new Point(600, 800);
-        mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID, BINDER,
-                size);
+        VirtualTouchscreenConfig positiveConfig =
+                new VirtualTouchscreenConfig.Builder()
+                        .setVendorId(VENDOR_ID)
+                        .setProductId(PRODUCT_ID)
+                        .setInputDeviceName(DEVICE_NAME)
+                        .setAssociatedDisplayId(DISPLAY_ID)
+                        .setWidthInPixels(600)
+                        .setHeightInPixels(800)
+                        .build();
+        mDeviceImpl.createVirtualTouchscreen(positiveConfig, BINDER);
         assertWithMessage(
-                "Virtual touchscreen should create input device descriptor on successful creation.")
-                .that(mInputController.mInputDeviceDescriptors).isNotEmpty();
+                "Virtual touchscreen should create input device descriptor on successful creation"
+                        + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty();
     }
 
     @Test
@@ -548,10 +595,8 @@
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
         doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
                 eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualDpad(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
-                        PRODUCT_ID, BINDER));
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
     }
 
     @Test
@@ -559,10 +604,8 @@
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
         doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
                 eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
-                        PRODUCT_ID, BINDER));
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
     }
 
     @Test
@@ -570,10 +613,8 @@
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
         doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
                 eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
-                        PRODUCT_ID, BINDER));
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
     }
 
     @Test
@@ -581,10 +622,8 @@
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
         doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
                 eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME,
-                        VENDOR_ID, PRODUCT_ID, BINDER, new Point(WIDTH, HEIGHT)));
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
     }
 
     @Test
@@ -619,21 +658,19 @@
     @Test
     public void createVirtualDpad_hasDisplay_obtainFileDescriptor() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
-        mDeviceImpl.createVirtualDpad(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID,
-                BINDER);
-        assertWithMessage("Virtual dpad should register fd when the display matches")
-          	.that(mInputController.mInputDeviceDescriptors).isNotEmpty();
-        verify(mNativeWrapperMock).openUinputDpad(eq(DEVICE_NAME), eq(VENDOR_ID),
-                eq(PRODUCT_ID), anyString());
+        mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER);
+        assertWithMessage("Virtual dpad should register fd when the display matches").that(
+                mInputController.getInputDeviceDescriptors()).isNotEmpty();
+        verify(mNativeWrapperMock).openUinputDpad(eq(DEVICE_NAME), eq(VENDOR_ID), eq(PRODUCT_ID),
+                anyString());
     }
 
     @Test
     public void createVirtualKeyboard_hasDisplay_obtainFileDescriptor() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
-        mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID,
-                BINDER);
-        assertWithMessage("Virtual keyboard should register fd when the display matches")
-                .that(mInputController.mInputDeviceDescriptors).isNotEmpty();
+        mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
+        assertWithMessage("Virtual keyboard should register fd when the display matches").that(
+                mInputController.getInputDeviceDescriptors()).isNotEmpty();
         verify(mNativeWrapperMock).openUinputKeyboard(eq(DEVICE_NAME), eq(VENDOR_ID),
                 eq(PRODUCT_ID), anyString());
     }
@@ -641,10 +678,9 @@
     @Test
     public void createVirtualMouse_hasDisplay_obtainFileDescriptor() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
-        mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID,
-                BINDER);
-        assertWithMessage("Virtual mouse should register fd when the display matches")
-                .that(mInputController.mInputDeviceDescriptors).isNotEmpty();
+        mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER);
+        assertWithMessage("Virtual mouse should register fd when the display matches").that(
+                mInputController.getInputDeviceDescriptors()).isNotEmpty();
         verify(mNativeWrapperMock).openUinputMouse(eq(DEVICE_NAME), eq(VENDOR_ID), eq(PRODUCT_ID),
                 anyString());
     }
@@ -652,10 +688,9 @@
     @Test
     public void createVirtualTouchscreen_hasDisplay_obtainFileDescriptor() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
-        mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID,
-                BINDER, new Point(WIDTH, HEIGHT));
-        assertWithMessage("Virtual touchscreen should register fd when the display matches")
-                .that(mInputController.mInputDeviceDescriptors).isNotEmpty();
+        mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER);
+        assertWithMessage("Virtual touchscreen should register fd when the display matches").that(
+                mInputController.getInputDeviceDescriptors()).isNotEmpty();
         verify(mNativeWrapperMock).openUinputTouchscreen(eq(DEVICE_NAME), eq(VENDOR_ID),
                 eq(PRODUCT_ID), anyString(), eq(HEIGHT), eq(WIDTH));
     }
@@ -909,9 +944,28 @@
         mDeviceImpl.mVirtualDisplayIds.add(1);
         mDeviceImpl.mVirtualDisplayIds.add(2);
         mDeviceImpl.mVirtualDisplayIds.add(3);
-        mDeviceImpl.createVirtualMouse(1, DEVICE_NAME, VENDOR_ID, PRODUCT_ID, BINDER);
-        mDeviceImpl.createVirtualMouse(2, DEVICE_NAME, VENDOR_ID, PRODUCT_ID, BINDER);
-        mDeviceImpl.createVirtualMouse(3, DEVICE_NAME, VENDOR_ID, PRODUCT_ID, BINDER);
+        VirtualMouseConfig config1 = new VirtualMouseConfig.Builder()
+                .setAssociatedDisplayId(1)
+                .setInputDeviceName(DEVICE_NAME)
+                .setVendorId(VENDOR_ID)
+                .setProductId(PRODUCT_ID)
+                .build();
+        VirtualMouseConfig config2 = new VirtualMouseConfig.Builder()
+                .setAssociatedDisplayId(2)
+                .setInputDeviceName(DEVICE_NAME)
+                .setVendorId(VENDOR_ID)
+                .setProductId(PRODUCT_ID)
+                .build();
+        VirtualMouseConfig config3 = new VirtualMouseConfig.Builder()
+                .setAssociatedDisplayId(3)
+                .setInputDeviceName(DEVICE_NAME)
+                .setVendorId(VENDOR_ID)
+                .setProductId(PRODUCT_ID)
+                .build();
+
+        mDeviceImpl.createVirtualMouse(config1, BINDER);
+        mDeviceImpl.createVirtualMouse(config2, BINDER);
+        mDeviceImpl.createVirtualMouse(config3, BINDER);
         mDeviceImpl.setShowPointerIcon(false);
         verify(mInputManagerInternalMock, times(3)).setPointerIconVisible(eq(false), anyInt());
         verify(mInputManagerInternalMock, never()).setPointerIconVisible(eq(true), anyInt());
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index 3ca648c..aaa1351 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.companion.virtual.audio;
 
-import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
 import static android.media.AudioAttributes.FLAG_SECURE;
 import static android.media.AudioPlaybackConfiguration.PLAYER_STATE_STARTED;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
@@ -86,8 +85,9 @@
                         /* pipBlockedCallback= */ null,
                         /* activityBlockedCallback= */ null,
                         /* secureWindowCallback= */ null,
-                        /* deviceProfile= */ DEVICE_PROFILE_APP_STREAMING,
-                        /* displayCategories= */ new ArrayList<>());
+                        /* displayCategories= */ new ArrayList<>(),
+                        /* recentsPolicy= */
+                        VirtualDeviceParams.RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS);
     }
 
 
diff --git a/services/tests/servicestests/src/com/android/server/content/SyncManagerTest.java b/services/tests/servicestests/src/com/android/server/content/SyncManagerTest.java
index c2a81d9..d4e3d44 100644
--- a/services/tests/servicestests/src/com/android/server/content/SyncManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/content/SyncManagerTest.java
@@ -16,12 +16,40 @@
 
 package com.android.server.content;
 
+import static android.content.pm.UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT;
+import static android.content.pm.UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT;
+import static android.content.pm.UserProperties.SHOW_IN_SETTINGS_WITH_PARENT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.accounts.AccountManagerInternal;
 import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.os.Bundle;
+import android.os.UserManager;
+import android.provider.ContactsContract;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.job.JobSchedulerInternal;
+
 import junit.framework.TestCase;
 
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
 /**
  * Tests for SyncManager.
  *
@@ -33,6 +61,43 @@
     final String KEY_1 = "key_1";
     final String KEY_2 = "key_2";
 
+    private SyncManager mSyncManager;
+    private Context mContext;
+
+    @Mock
+    private UserManager mUserManager;
+    @Mock
+    private AccountManagerInternal mAccountManagerInternal;
+    @Mock
+    private JobSchedulerInternal mJobSchedulerInternal;
+
+    private class SyncManagerWithMockedServices extends SyncManager {
+
+        @Override
+        protected AccountManagerInternal getAccountManagerInternal() {
+            return mAccountManagerInternal;
+        }
+
+        @Override
+        protected JobSchedulerInternal getJobSchedulerInternal() {
+            return mJobSchedulerInternal;
+        }
+
+        private SyncManagerWithMockedServices(Context context, boolean factoryTest) {
+            super(context, factoryTest);
+        }
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+        doNothing().when(mAccountManagerInternal).addOnAppPermissionChangeListener(any());
+        when(mJobSchedulerInternal.getSystemScheduledPendingJobs()).thenReturn(new ArrayList<>());
+        mSyncManager = new SyncManagerWithMockedServices(mContext, true);
+    }
+
     public void testSyncExtrasEquals_WithNull() throws Exception {
         Bundle b1 = new Bundle();
         Bundle b2 = new Bundle();
@@ -140,4 +205,45 @@
         final StringBuilder sb = new StringBuilder();
         assertEquals(expected, SyncManager.formatDurationHMS(sb, time * 1000).toString());
     }
+
+    private UserInfo createUserInfo(String name, int id, int groupId, int flags) {
+        final UserInfo ui = new UserInfo(id, name, flags | UserInfo.FLAG_INITIALIZED);
+        ui.profileGroupId = groupId;
+        return ui;
+    }
+
+    @NotNull
+    private UserProperties getCloneUserProperties() {
+        return new UserProperties.Builder()
+                .setStartWithParent(true)
+                .setShowInLauncher(SHOW_IN_LAUNCHER_WITH_PARENT)
+                .setShowInSettings(SHOW_IN_SETTINGS_WITH_PARENT)
+                .setUseParentsContacts(true)
+                .setInheritDevicePolicy(INHERIT_DEVICE_POLICY_FROM_PARENT)
+                .build();
+    }
+
+    private void mockUserProperties(UserInfo primaryUserInfo, UserInfo cloneUserInfo) {
+        UserProperties cloneUserProperties = getCloneUserProperties();
+        when(mUserManager.getUserProperties(cloneUserInfo.getUserHandle()))
+                .thenReturn(cloneUserProperties);
+        // Set default user properties for primary user
+        when(mUserManager.getUserProperties(primaryUserInfo.getUserHandle()))
+                .thenReturn(new UserProperties.Builder().build());
+    }
+
+    public void testShouldDisableSync() {
+        UserInfo primaryUserInfo = createUserInfo("primary", 0 /* id */, 0 /* groupId */,
+                UserInfo.FLAG_PRIMARY | UserInfo.FLAG_ADMIN);
+        UserInfo cloneUserInfo = createUserInfo("clone", 10 /* id */, 0 /* groupId */,
+                UserInfo.FLAG_PROFILE);
+
+        mockUserProperties(primaryUserInfo, cloneUserInfo);
+
+        // Clone user accounts must have contact syncs disabled
+        assertThat(mSyncManager.shouldDisableSyncForUser(cloneUserInfo,
+                ContactsContract.AUTHORITY)).isTrue();
+        assertThat(mSyncManager.shouldDisableSyncForUser(primaryUserInfo,
+                ContactsContract.AUTHORITY)).isFalse();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index 0206839..ae36871 100644
--- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
@@ -176,7 +177,7 @@
 
         // Send new sensor value and verify
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1));
-        assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), 0.001f);
+        assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON);
 
         // Set up system to return 0.0f (minimum possible brightness) as a brightness value
         float lux2 = 10.0f;
@@ -190,7 +191,7 @@
 
         // Send new sensor value and verify
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2));
-        assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), 0.001f);
+        assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON);
     }
 
     @Test
@@ -219,7 +220,7 @@
 
         // Send new sensor value and verify
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1));
-        assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), 0.001f);
+        assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON);
 
 
         // Set up system to return 1.0f as a brightness value (brightness_max)
@@ -234,7 +235,7 @@
 
         // Send new sensor value and verify
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2));
-        assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), 0.001f);
+        assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON);
     }
 
     @Test
@@ -416,6 +417,12 @@
         // ambient lux goes to 0
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
         assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // only the values within the horizon should be kept
+        assertArrayEquals(new float[] {10000, 10000, 0, 0, 0}, mController.getLastSensorValues(),
+                EPSILON);
+        assertArrayEquals(new long[] {4000, 4500, 5000, 5500, 6000},
+                mController.getLastSensorTimestamps());
     }
 
     @Test
@@ -487,4 +494,92 @@
                 0 /* adjustment */, false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
         assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getAutomaticScreenBrightness(), 0.0f);
     }
+
+    @Test
+    public void testGetSensorReadings() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // Choose values such that the ring buffer's capacity is extended and the buffer is pruned
+        int increment = 11;
+        int lux = 5000;
+        for (int i = 0; i < 1000; i++) {
+            lux += increment;
+            mClock.fastForward(increment);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment + 1);
+        float[] sensorValues = mController.getLastSensorValues();
+        long[] sensorTimestamps = mController.getLastSensorTimestamps();
+
+        // Only the values within the horizon should be kept
+        assertEquals(valuesCount, sensorValues.length);
+        assertEquals(valuesCount, sensorTimestamps.length);
+
+        long sensorTimestamp = mClock.now();
+        for (int i = valuesCount - 1; i >= 1; i--) {
+            assertEquals(lux, sensorValues[i], EPSILON);
+            assertEquals(sensorTimestamp, sensorTimestamps[i]);
+            lux -= increment;
+            sensorTimestamp -= increment;
+        }
+        assertEquals(lux, sensorValues[0], EPSILON);
+        assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
+    }
+
+    @Test
+    public void testGetSensorReadingsFullBuffer() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+        int initialCapacity = 150;
+
+        // Choose values such that the ring buffer is pruned
+        int increment1 = 200;
+        int lux = 5000;
+        for (int i = 0; i < 20; i++) {
+            lux += increment1;
+            mClock.fastForward(increment1);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment1 + 1);
+
+        // Choose values such that the buffer becomes full
+        int increment2 = 1;
+        for (int i = 0; i < initialCapacity - valuesCount; i++) {
+            lux += increment2;
+            mClock.fastForward(increment2);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        float[] sensorValues = mController.getLastSensorValues();
+        long[] sensorTimestamps = mController.getLastSensorTimestamps();
+
+        // The buffer should be full
+        assertEquals(initialCapacity, sensorValues.length);
+        assertEquals(initialCapacity, sensorTimestamps.length);
+
+        long sensorTimestamp = mClock.now();
+        for (int i = initialCapacity - 1; i >= 1; i--) {
+            assertEquals(lux, sensorValues[i], EPSILON);
+            assertEquals(sensorTimestamp, sensorTimestamps[i]);
+
+            if (i >= valuesCount) {
+                lux -= increment2;
+                sensorTimestamp -= increment2;
+            } else {
+                lux -= increment1;
+                sensorTimestamp -= increment1;
+            }
+        }
+        assertEquals(lux, sensorValues[0], EPSILON);
+        assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
index c2e8417f..6def7b1 100644
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
@@ -136,28 +136,28 @@
         assertNull(mInjector.mSensorListener);
         assertNotNull(mInjector.mBroadcastReceiver);
         assertTrue(mInjector.mIdleScheduled);
-        mInjector.sendScreenChange(/*screen on */ true);
+        mInjector.sendScreenChange(/* screenOn= */ true);
         assertNotNull(mInjector.mSensorListener);
         assertTrue(mInjector.mColorSamplingEnabled);
 
-        mInjector.sendScreenChange(/*screen on */ false);
+        mInjector.sendScreenChange(/* screenOn= */ false);
         assertNull(mInjector.mSensorListener);
         assertFalse(mInjector.mColorSamplingEnabled);
 
         // Turn screen on while brightness mode is manual
-        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic */ false);
-        mInjector.sendScreenChange(/*screen on */ true);
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ false);
+        mInjector.sendScreenChange(/* screenOn= */ true);
         assertNull(mInjector.mSensorListener);
         assertFalse(mInjector.mColorSamplingEnabled);
 
         // Set brightness mode to automatic while screen is off.
-        mInjector.sendScreenChange(/*screen on */ false);
-        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic */ true);
+        mInjector.sendScreenChange(/* screenOn= */ false);
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
         assertNull(mInjector.mSensorListener);
         assertFalse(mInjector.mColorSamplingEnabled);
 
         // Turn on screen while brightness mode is automatic.
-        mInjector.sendScreenChange(/*screen on */ true);
+        mInjector.sendScreenChange(/* screenOn= */ true);
         assertNotNull(mInjector.mSensorListener);
         assertTrue(mInjector.mColorSamplingEnabled);
 
@@ -188,14 +188,14 @@
         assertFalse(mInjector.mColorSamplingEnabled);
 
         // Pretend screen is off, update config to turn on color sampling.
-        mInjector.sendScreenChange(/*screen on */ false);
+        mInjector.sendScreenChange(/* screenOn= */ false);
         mTracker.setBrightnessConfiguration(buildBrightnessConfiguration(
                 /* collectColorSamples= */ true));
         mInjector.waitForHandler();
         assertFalse(mInjector.mColorSamplingEnabled);
 
         // Pretend screen is on.
-        mInjector.sendScreenChange(/*screen on */ true);
+        mInjector.sendScreenChange(/* screenOn= */ true);
         assertTrue(mInjector.mColorSamplingEnabled);
 
         mTracker.stop();
@@ -261,7 +261,7 @@
         assertFalse(mInjector.mColorSamplingEnabled);
         assertNull(mInjector.mDisplayListener);
 
-        mInjector.setBrightnessMode(/*isBrightnessModeAutomatic*/ true);
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
         assertNotNull(mInjector.mSensorListener);
         assertTrue(mInjector.mColorSamplingEnabled);
         assertNotNull(mInjector.mDisplayListener);
@@ -272,16 +272,15 @@
         mInjector.mColorSamplingEnabled = false;
         mInjector.mDisplayListener = null;
         // Duplicate notification
-        mInjector.setBrightnessMode(/*isBrightnessModeAutomatic*/ true);
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
         // Sensor shouldn't have been registered as it was already registered.
         assertNull(mInjector.mSensorListener);
         assertFalse(mInjector.mColorSamplingEnabled);
         assertNull(mInjector.mDisplayListener);
-        mInjector.mSensorListener = listener;
         mInjector.mDisplayListener = displayListener;
         mInjector.mColorSamplingEnabled = true;
 
-        mInjector.setBrightnessMode(/*isBrightnessModeAutomatic*/ false);
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ false);
         assertNull(mInjector.mSensorListener);
         assertFalse(mInjector.mColorSamplingEnabled);
         assertNull(mInjector.mDisplayListener);
@@ -301,19 +300,21 @@
         final String displayId = "1234";
 
         startTracker(mTracker);
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
+        final long sensorTime = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
         mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-        notifyBrightnessChanged(mTracker, brightness, displayId);
+        final long currentTime = mInjector.currentTimeMillis();
+        notifyBrightnessChanged(mTracker, brightness, displayId, new float[] {1.0f},
+                new long[] {sensorTime});
         List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
         mTracker.stop();
 
         assertEquals(1, events.size());
         BrightnessChangeEvent event = events.get(0);
-        assertEquals(mInjector.currentTimeMillis(), event.timeStamp);
+        assertEquals(currentTime, event.timeStamp);
         assertEquals(displayId, event.uniqueDisplayId);
         assertEquals(1, event.luxValues.length);
         assertEquals(1.0f, event.luxValues[0], FLOAT_DELTA);
-        assertEquals(mInjector.currentTimeMillis() - TimeUnit.SECONDS.toMillis(2),
+        assertEquals(currentTime - TimeUnit.SECONDS.toMillis(2),
                 event.luxTimestamps[0]);
         assertEquals(brightness, event.brightness, FLOAT_DELTA);
         assertEquals(DEFAULT_INITIAL_BRIGHTNESS, event.lastBrightness, FLOAT_DELTA);
@@ -339,9 +340,9 @@
         startTracker(mTracker, initialBrightness, DEFAULT_COLOR_SAMPLING_ENABLED);
         mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
                 batteryChangeEvent(30, 60));
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1000.0f));
-        final long sensorTime = mInjector.currentTimeMillis();
-        notifyBrightnessChanged(mTracker, brightness, displayId);
+        final long currentTime = mInjector.currentTimeMillis();
+        notifyBrightnessChanged(mTracker, brightness, displayId, new float[] {1000.0f},
+                new long[] {TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos())});
         List<BrightnessChangeEvent> eventsNoPackage
                 = mTracker.getEvents(0, false).getList();
         List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
@@ -349,10 +350,10 @@
 
         assertEquals(1, events.size());
         BrightnessChangeEvent event = events.get(0);
-        assertEquals(event.timeStamp, mInjector.currentTimeMillis());
+        assertEquals(event.timeStamp, currentTime);
         assertEquals(displayId, event.uniqueDisplayId);
-        assertArrayEquals(new float[] {1000.0f}, event.luxValues, 0.01f);
-        assertArrayEquals(new long[] {sensorTime}, event.luxTimestamps);
+        assertArrayEquals(new float[] {1000.0f}, event.luxValues, FLOAT_DELTA);
+        assertArrayEquals(new long[] {currentTime}, event.luxTimestamps);
         assertEquals(brightness, event.brightness, FLOAT_DELTA);
         assertEquals(initialBrightness, event.lastBrightness, FLOAT_DELTA);
         assertEquals(0.5, event.batteryLevel, FLOAT_DELTA);
@@ -374,13 +375,12 @@
     public void testIgnoreAutomaticBrightnessChange() {
         final int initialBrightness = 30;
         startTracker(mTracker, initialBrightness, DEFAULT_COLOR_SAMPLING_ENABLED);
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
         mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1));
 
         final int systemUpdatedBrightness = 20;
-        notifyBrightnessChanged(mTracker, systemUpdatedBrightness, false /*userInitiated*/,
-                0.5f /*powerBrightnessFactor(*/, false /*isUserSetBrightness*/,
-                false /*isDefaultBrightnessConfig*/, DEFAULT_DISPLAY_ID);
+        notifyBrightnessChanged(mTracker, systemUpdatedBrightness, /* userInitiated= */ false,
+                /* powerBrightnessFactor= */ 0.5f, /* isUserSetBrightness= */ false,
+                /* isDefaultBrightnessConfig= */ false, DEFAULT_DISPLAY_ID);
         List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
         // No events because we filtered out our change.
         assertEquals(0, events.size());
@@ -408,10 +408,8 @@
     @Test
     public void testLimitedBufferSize() {
         startTracker(mTracker);
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
 
         for (int brightness = 0; brightness <= 255; ++brightness) {
-            mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
             mInjector.incrementTime(TimeUnit.SECONDS.toNanos(1));
             notifyBrightnessChanged(mTracker, brightness);
         }
@@ -427,33 +425,6 @@
     }
 
     @Test
-    public void testLimitedSensorEvents() {
-        final int brightness = 20;
-
-        startTracker(mTracker);
-        // 20 Sensor events 1 second apart.
-        for (int i = 0; i < 20; ++i) {
-            mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1));
-            mInjector.mSensorListener.onSensorChanged(createSensorEvent(i + 1.0f));
-        }
-        notifyBrightnessChanged(mTracker, 20);
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        mTracker.stop();
-
-        assertEquals(1, events.size());
-        BrightnessChangeEvent event = events.get(0);
-        assertEquals(mInjector.currentTimeMillis(), event.timeStamp);
-
-        // 12 sensor events, 11 for 0->10 seconds + 1 previous event.
-        assertEquals(12, event.luxValues.length);
-        for (int i = 0; i < 12; ++i) {
-            assertEquals(event.luxTimestamps[11 - i],
-                    mInjector.currentTimeMillis() - i * TimeUnit.SECONDS.toMillis(1));
-        }
-        assertEquals(brightness, event.brightness, FLOAT_DELTA);
-    }
-
-    @Test
     public void testReadEvents() throws Exception {
         BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
                 mInjector);
@@ -607,15 +578,16 @@
         startTracker(mTracker);
         mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
                 batteryChangeEvent(30, 100));
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(2000.0f));
-        final long firstSensorTime = mInjector.currentTimeMillis();
+        final long elapsedTime1 = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
+        final long currentTime1 = mInjector.currentTimeMillis();
         mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(3000.0f));
-        final long secondSensorTime = mInjector.currentTimeMillis();
+        final long elapsedTime2 = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
+        final long currentTime2 = mInjector.currentTimeMillis();
         mInjector.incrementTime(TimeUnit.SECONDS.toMillis(3));
-        notifyBrightnessChanged(mTracker, brightness, true /*userInitiated*/,
-                0.5f /*powerBrightnessFactor*/, true /*hasUserBrightnessPoints*/,
-                false /*isDefaultBrightnessConfig*/, displayId);
+        notifyBrightnessChanged(mTracker, brightness, /* userInitiated= */ true,
+                /* powerBrightnessFactor= */ 0.5f, /* isUserSetBrightness= */ true,
+                /* isDefaultBrightnessConfig= */ false, displayId, new float[] {2000.0f, 3000.0f},
+                new long[] {elapsedTime1, elapsedTime2});
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         mTracker.writeEventsLocked(baos);
         mTracker.stop();
@@ -631,7 +603,7 @@
         BrightnessChangeEvent event = events.get(0);
         assertEquals(displayId, event.uniqueDisplayId);
         assertArrayEquals(new float[] {2000.0f, 3000.0f}, event.luxValues, FLOAT_DELTA);
-        assertArrayEquals(new long[] {firstSensorTime, secondSensorTime}, event.luxTimestamps);
+        assertArrayEquals(new long[] {currentTime1, currentTime2}, event.luxTimestamps);
         assertEquals(brightness, event.brightness, FLOAT_DELTA);
         assertEquals(0.3, event.batteryLevel, FLOAT_DELTA);
         assertTrue(event.nightMode);
@@ -647,53 +619,6 @@
     }
 
     @Test
-    public void testWritePrunesOldEvents() throws Exception {
-        final int brightness = 20;
-
-        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1);
-        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3339);
-
-        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, 1);
-        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_LEVEL, 40);
-
-        startTracker(mTracker);
-        mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
-                batteryChangeEvent(30, 100));
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1000.0f));
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1));
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(2000.0f));
-        final long sensorTime = mInjector.currentTimeMillis();
-        notifyBrightnessChanged(mTracker, brightness);
-
-        // 31 days later
-        mInjector.incrementTime(TimeUnit.DAYS.toMillis(31));
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(3000.0f));
-        notifyBrightnessChanged(mTracker, brightness);
-        final long eventTime = mInjector.currentTimeMillis();
-
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        assertEquals(2, events.size());
-
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mTracker.writeEventsLocked(baos);
-        events = mTracker.getEvents(0, true).getList();
-        mTracker.stop();
-
-        assertEquals(1, events.size());
-        BrightnessChangeEvent event = events.get(0);
-        assertEquals(eventTime, event.timeStamp);
-
-        // We will keep one of the old sensor events because we keep 1 event outside the window.
-        assertArrayEquals(new float[] {2000.0f, 3000.0f}, event.luxValues, FLOAT_DELTA);
-        assertArrayEquals(new long[] {sensorTime, eventTime}, event.luxTimestamps);
-        assertEquals(brightness, event.brightness, FLOAT_DELTA);
-        assertEquals(0.3, event.batteryLevel, FLOAT_DELTA);
-        assertTrue(event.nightMode);
-        assertTrue(event.reduceBrightColors);
-        assertEquals(3339, event.colorTemperature);
-    }
-
-    @Test
     public void testParcelUnParcel() {
         Parcel parcel = Parcel.obtain();
         BrightnessChangeEvent.Builder builder = new BrightnessChangeEvent.Builder();
@@ -796,9 +721,10 @@
 
         // Send an event.
         long eventTime = mInjector.currentTimeMillis();
-        mTracker.notifyBrightnessChanged(brightness, true /*userInitiated*/,
-                1.0f /*powerBrightnessFactor*/, false /*isUserSetBrightness*/,
-                false /*isDefaultBrightnessConfig*/, DEFAULT_DISPLAY_ID);
+        mTracker.notifyBrightnessChanged(brightness, /* userInitiated= */ true,
+                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
+                /* isDefaultBrightnessConfig= */ false, DEFAULT_DISPLAY_ID, new float[10],
+                new long[10]);
 
         // Time passes before handler can run.
         mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
@@ -890,20 +816,33 @@
     public void testOnlyOneReceiverRegistered() {
         assertNull(mInjector.mLightSensor);
         assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mContentObserver);
+        assertNull(mInjector.mBroadcastReceiver);
+        assertFalse(mInjector.mIdleScheduled);
         startTracker(mTracker, 0.3f, false);
 
         assertNotNull(mInjector.mLightSensor);
         assertNotNull(mInjector.mSensorListener);
+        assertNotNull(mInjector.mContentObserver);
+        assertNotNull(mInjector.mBroadcastReceiver);
+        assertTrue(mInjector.mIdleScheduled);
         Sensor registeredLightSensor = mInjector.mLightSensor;
         SensorEventListener registeredSensorListener = mInjector.mSensorListener;
+        ContentObserver registeredContentObserver = mInjector.mContentObserver;
+        BroadcastReceiver registeredBroadcastReceiver = mInjector.mBroadcastReceiver;
 
         mTracker.start(0.3f);
         assertSame(registeredLightSensor, mInjector.mLightSensor);
         assertSame(registeredSensorListener, mInjector.mSensorListener);
+        assertSame(registeredContentObserver, mInjector.mContentObserver);
+        assertSame(registeredBroadcastReceiver, mInjector.mBroadcastReceiver);
 
         mTracker.stop();
         assertNull(mInjector.mLightSensor);
         assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mContentObserver);
+        assertNull(mInjector.mBroadcastReceiver);
+        assertFalse(mInjector.mIdleScheduled);
 
         // mInjector asserts that we aren't removing a null receiver
         mTracker.stop();
@@ -954,23 +893,41 @@
 
     private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
             String displayId) {
-        notifyBrightnessChanged(tracker, brightness, true /*userInitiated*/,
-                1.0f /*powerBrightnessFactor*/, false /*isUserSetBrightness*/,
-                false /*isDefaultBrightnessConfig*/, displayId);
+        notifyBrightnessChanged(tracker, brightness, /* userInitiated= */ true,
+                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
+                /* isDefaultBrightnessConfig= */ false, displayId, new float[10], new long[10]);
+    }
+
+    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
+            String displayId, float[] luxValues, long[] luxTimestamps) {
+        notifyBrightnessChanged(tracker, brightness, /* userInitiated= */ true,
+                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
+                /* isDefaultBrightnessConfig= */ false, displayId, luxValues, luxTimestamps);
     }
 
     private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
             boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness,
             boolean isDefaultBrightnessConfig, String displayId) {
         tracker.notifyBrightnessChanged(brightness, userInitiated, powerBrightnessFactor,
-                isUserSetBrightness, isDefaultBrightnessConfig, displayId);
+                isUserSetBrightness, isDefaultBrightnessConfig, displayId, new float[10],
+                new long[10]);
+        mInjector.waitForHandler();
+    }
+
+    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
+            boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness,
+            boolean isDefaultBrightnessConfig, String displayId, float[] luxValues,
+            long[] luxTimestamps) {
+        tracker.notifyBrightnessChanged(brightness, userInitiated, powerBrightnessFactor,
+                isUserSetBrightness, isDefaultBrightnessConfig, displayId, luxValues,
+                luxTimestamps);
         mInjector.waitForHandler();
     }
 
     private BrightnessConfiguration buildBrightnessConfiguration(boolean collectColorSamples) {
         BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder(
-                /* lux = */ new float[] {0f, 10f, 100f},
-                /* nits = */ new float[] {1f, 90f, 100f});
+                /* lux= */ new float[] {0f, 10f, 100f},
+                /* nits= */ new float[] {1f, 90f, 100f});
         builder.setShouldCollectColorSamples(collectColorSamples);
         return builder.build();
     }
diff --git a/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java b/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
new file mode 100644
index 0000000..bcae50e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.display;
+
+
+import static org.junit.Assert.assertEquals;
+
+import android.view.DisplayAddress;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.display.layout.DisplayIdProducer;
+import com.android.server.display.layout.Layout;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+
+@SmallTest
+public class DeviceStateToLayoutMapTest {
+    private DeviceStateToLayoutMap mDeviceStateToLayoutMap;
+
+    @Mock DisplayIdProducer mDisplayIdProducerMock;
+
+    @Before
+    public void setUp() throws IOException {
+        MockitoAnnotations.initMocks(this);
+
+        Mockito.when(mDisplayIdProducerMock.getId(false)).thenReturn(1);
+
+        setupDeviceStateToLayoutMap();
+    }
+
+    //////////////////
+    // Test Methods //
+    //////////////////
+
+    @Test
+    public void testInitialState() {
+        Layout configLayout = mDeviceStateToLayoutMap.get(0);
+
+        Layout testLayout = new Layout();
+        testLayout.createDisplayLocked(
+                DisplayAddress.fromPhysicalDisplayId(123456L), /* isDefault= */ true,
+                /* isEnabled= */ true, mDisplayIdProducerMock);
+        testLayout.createDisplayLocked(
+                DisplayAddress.fromPhysicalDisplayId(78910L), /* isDefault= */ false,
+                /* isEnabled= */ false, mDisplayIdProducerMock);
+        assertEquals(testLayout, configLayout);
+    }
+
+    @Test
+    public void testSwitchedState() {
+        Layout configLayout = mDeviceStateToLayoutMap.get(1);
+
+        Layout testLayout = new Layout();
+        testLayout.createDisplayLocked(
+                DisplayAddress.fromPhysicalDisplayId(78910L), /* isDefault= */ true,
+                /* isEnabled= */ true, mDisplayIdProducerMock);
+        testLayout.createDisplayLocked(
+                DisplayAddress.fromPhysicalDisplayId(123456L), /* isDefault= */ false,
+                /* isEnabled= */ false, mDisplayIdProducerMock);
+
+        assertEquals(testLayout, configLayout);
+    }
+
+    @Test
+    public void testConcurrentState() {
+        Layout configLayout = mDeviceStateToLayoutMap.get(2);
+
+        Layout testLayout = new Layout();
+        testLayout.createDisplayLocked(
+                DisplayAddress.fromPhysicalDisplayId(345L), /* isDefault= */ true,
+                /* isEnabled= */ true, mDisplayIdProducerMock);
+        testLayout.createDisplayLocked(
+                DisplayAddress.fromPhysicalDisplayId(678L), /* isDefault= */ false,
+                /* isEnabled= */ true, mDisplayIdProducerMock);
+
+        assertEquals(testLayout, configLayout);
+    }
+
+    ////////////////////
+    // Helper Methods //
+    ////////////////////
+
+    private void setupDeviceStateToLayoutMap() throws IOException {
+        Path tempFile = Files.createTempFile("device_state_layout_map", ".tmp");
+        Files.write(tempFile, getContent().getBytes(StandardCharsets.UTF_8));
+        mDeviceStateToLayoutMap = new DeviceStateToLayoutMap(mDisplayIdProducerMock,
+                tempFile.toFile());
+    }
+
+    private String getContent() {
+        return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                +  "<layouts>\n"
+                +    "<layout>\n"
+                +      "<state>0</state> \n"
+                +      "<display enabled=\"true\" defaultDisplay=\"true\">\n"
+                +        "<address>123456</address>\n"
+                +      "</display>\n"
+                +      "<display enabled=\"false\">\n"
+                +        "<address>78910</address>\n"
+                +      "</display>\n"
+                +    "</layout>\n"
+
+                +    "<layout>\n"
+                +      "<state>1</state> \n"
+                +      "<display enabled=\"true\" defaultDisplay=\"true\">\n"
+                +        "<address>78910</address>\n"
+                +      "</display>\n"
+                +      "<display enabled=\"false\">\n"
+                +        "<address>123456</address>\n"
+                +      "</display>\n"
+                +    "</layout>\n"
+
+                +    "<layout>\n"
+                +      "<state>2</state> \n"
+                +      "<display enabled=\"true\" defaultDisplay=\"true\">\n"
+                +        "<address>345</address>\n"
+                +      "</display>\n"
+                +      "<display enabled=\"true\">\n"
+                +        "<address>678</address>\n"
+                +      "</display>\n"
+                +    "</layout>\n"
+                +  "</layouts>\n";
+    }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 5af0558..ca8e45a 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -16,7 +16,9 @@
 
 package com.android.server.display;
 
+import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 
@@ -724,9 +726,6 @@
         registerDefaultDisplays(displayManager);
         when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
 
-        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
-                .thenReturn(PackageManager.PERMISSION_DENIED);
-
         IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
         when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
                 .thenReturn(true);
@@ -780,9 +779,6 @@
         registerDefaultDisplays(displayManager);
         when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
 
-        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
-                .thenReturn(PackageManager.PERMISSION_DENIED);
-
         IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
         when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
                 .thenReturn(true);
@@ -792,7 +788,7 @@
         // virtual device.
         final VirtualDisplayConfig.Builder builder1 =
                 new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- device display group 1");
+                        .setUniqueId("uniqueId --- device display group");
 
         int displayId1 =
                 localService.createVirtualDisplay(
@@ -808,7 +804,7 @@
         final VirtualDisplayConfig.Builder builder2 =
                 new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
                         .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
-                        .setUniqueId("uniqueId --- device display group 1");
+                        .setUniqueId("uniqueId --- own display group");
 
         int displayId2 =
                 localService.createVirtualDisplay(
@@ -827,6 +823,99 @@
     }
 
     @Test
+    public void displaysInDeviceOrOwnDisplayGroupShouldPreserveAlwaysUnlockedFlag()
+            throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
+                .thenReturn(true);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+
+        // Allow an ALWAYS_UNLOCKED display to be created.
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+        when(mContext.checkCallingPermission(ADD_ALWAYS_UNLOCKED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+        // Create a virtual display in a device display group.
+        final VirtualDisplayConfig deviceDisplayGroupDisplayConfig =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1")
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
+                        .build();
+
+        int deviceDisplayGroupDisplayId =
+                localService.createVirtualDisplay(
+                        deviceDisplayGroupDisplayConfig,
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+
+        // Check that FLAG_ALWAYS_UNLOCKED is set.
+        assertNotEquals(
+                "FLAG_ALWAYS_UNLOCKED should be set for displays created in a device display"
+                        + " group.",
+                (displayManager.getDisplayDeviceInfoInternal(deviceDisplayGroupDisplayId).flags
+                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
+                0);
+
+        // Create a virtual display in its own display group.
+        final VirtualDisplayConfig ownDisplayGroupConfig =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- own display group 1")
+                        .setFlags(
+                                VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
+                                        | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+                        .build();
+
+        int ownDisplayGroupDisplayId =
+                localService.createVirtualDisplay(
+                        ownDisplayGroupConfig,
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+
+        // Check that FLAG_ALWAYS_UNLOCKED is set.
+        assertNotEquals(
+                "FLAG_ALWAYS_UNLOCKED should be set for displays created in their own display"
+                        + " group.",
+                (displayManager.getDisplayDeviceInfoInternal(ownDisplayGroupDisplayId).flags
+                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
+                0);
+
+        // Create a virtual display in a device display group.
+        final VirtualDisplayConfig defaultDisplayGroupConfig =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- default display group 1")
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
+                        .build();
+
+        int defaultDisplayGroupDisplayId =
+                localService.createVirtualDisplay(
+                        defaultDisplayGroupConfig,
+                        mMockAppToken /* callback */,
+                        null /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+
+        // Check that FLAG_ALWAYS_UNLOCKED is not set.
+        assertEquals(
+                "FLAG_ALWAYS_UNLOCKED should not be set for displays created in the default"
+                        + " display group.",
+                (displayManager.getDisplayDeviceInfoInternal(defaultDisplayGroupDisplayId).flags
+                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
+                0);
+    }
+
+    @Test
     public void testGetDisplayIdToMirror() throws Exception {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         registerDefaultDisplays(displayManager);
@@ -1164,6 +1253,76 @@
     }
 
     /**
+     * Tests that there is a display change notification if the render frame rate is updated
+     */
+    @Test
+    public void testShouldNotifyChangeWhenDisplayInfoRenderFrameRateChanged() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
+        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(displayManager,
+                displayManagerBinderService, displayDevice);
+
+        updateRenderFrameRate(displayManager, displayDevice, 30f);
+        assertTrue(callback.mDisplayChangedCalled);
+        callback.clear();
+
+        updateRenderFrameRate(displayManager, displayDevice, 30f);
+        assertFalse(callback.mDisplayChangedCalled);
+
+        updateRenderFrameRate(displayManager, displayDevice, 20f);
+        assertTrue(callback.mDisplayChangedCalled);
+        callback.clear();
+    }
+
+    /**
+     * Tests that the DisplayInfo is updated correctly with a render frame rate
+     */
+    @Test
+    public void testDisplayInfoRenderFrameRate() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateRenderFrameRate(displayManager, displayDevice, 20f);
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+    }
+
+    /**
+     * Tests that the mode reflects the render frame rate is in compat mode
+     */
+    @Test
+    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public  void testDisplayInfoRenderFrameRateModeCompat() throws Exception {
+        testDisplayInfoRenderFrameRateModeCompat(/*compatChangeEnabled*/ false);
+    }
+
+    /**
+     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
+     */
+    @Test
+    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public  void testDisplayInfoRenderFrameRateMode() throws Exception {
+        testDisplayInfoRenderFrameRateModeCompat(/*compatChangeEnabled*/ true);
+    }
+
+    /**
      * Tests that EVENT_DISPLAY_ADDED is sent when a display is added.
      */
     @Test
@@ -1383,6 +1542,34 @@
         assertEquals(expectedMode, displayInfo.getMode());
     }
 
+    private void testDisplayInfoRenderFrameRateModeCompat(boolean compatChangeEnabled)
+            throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateRenderFrameRate(displayManager, displayDevice, 20f);
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+        Display.Mode expectedMode;
+        if (compatChangeEnabled) {
+            expectedMode = new Display.Mode(1, 100, 200, 60f);
+        } else {
+            expectedMode = new Display.Mode(3, 100, 200, 20f);
+        }
+        assertEquals(expectedMode, displayInfo.getMode());
+    }
+
     private void testDisplayInfoNonNativeFrameRateOverrideMode(boolean compatChangeEnabled) {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mBasicInjector);
@@ -1452,6 +1639,15 @@
         updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
     }
 
+    private void updateRenderFrameRate(DisplayManagerService displayManager,
+            FakeDisplayDevice displayDevice,
+            float renderFrameRate) {
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
+        displayDeviceInfo.renderFrameRate = renderFrameRate;
+        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
+    }
+
     private void updateModeId(DisplayManagerService displayManager,
             FakeDisplayDevice displayDevice,
             int modeId) {
@@ -1491,6 +1687,7 @@
                     new Display.Mode(i + 1, width, height, refreshRates[i]);
         }
         displayDeviceInfo.modeId = 1;
+        displayDeviceInfo.renderFrameRate = displayDeviceInfo.supportedModes[0].getRefreshRate();
         displayDeviceInfo.width = width;
         displayDeviceInfo.height = height;
         final Rect zeroRect = new Rect();
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 246945c..c7caa43 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -54,6 +54,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
 
 import org.junit.Before;
@@ -76,6 +77,7 @@
     private static int sUniqueTestDisplayId = 0;
     private static final int DEVICE_STATE_CLOSED = 0;
     private static final int DEVICE_STATE_OPEN = 2;
+    private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1;
 
     private DisplayDeviceRepository mDisplayDeviceRepo;
     private LogicalDisplayMapper mLogicalDisplayMapper;
@@ -83,12 +85,16 @@
     private Handler mHandler;
     private PowerManager mPowerManager;
 
+    private final DisplayIdProducer mIdProducer = (isDefault) ->
+            isDefault ? DEFAULT_DISPLAY : sNextNonDefaultDisplayId++;
+
     @Mock LogicalDisplayMapper.Listener mListenerMock;
     @Mock Context mContextMock;
     @Mock Resources mResourcesMock;
     @Mock IPowerManager mIPowerManagerMock;
     @Mock IThermalService mIThermalServiceMock;
-    @Spy DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy = new DeviceStateToLayoutMap();
+    @Spy DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy =
+            new DeviceStateToLayoutMap(mIdProducer);
 
     @Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor;
 
@@ -519,13 +525,17 @@
                 DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
         Layout layout = new Layout();
-        layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, true, true);
-        layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, false);
+        layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
+                true, true, mIdProducer);
+        layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
+                false, false, mIdProducer);
         when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout);
 
         layout = new Layout();
-        layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, false, false);
-        layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, true, true);
+        layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
+                false, false, mIdProducer);
+        layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
+                true, true, mIdProducer);
         when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(layout);
         when(mDeviceStateToLayoutMapSpy.get(2)).thenReturn(layout);
 
@@ -580,15 +590,18 @@
         threeDevicesEnabledLayout.createDisplayLocked(
                 displayAddressOne,
                 /* isDefault= */ true,
-                /* isEnabled= */ true);
+                /* isEnabled= */ true,
+                mIdProducer);
         threeDevicesEnabledLayout.createDisplayLocked(
                 displayAddressTwo,
                 /* isDefault= */ false,
-                /* isEnabled= */ true);
+                /* isEnabled= */ true,
+                mIdProducer);
         threeDevicesEnabledLayout.createDisplayLocked(
                 displayAddressThree,
                 /* isDefault= */ false,
-                /* isEnabled= */ true);
+                /* isEnabled= */ true,
+                mIdProducer);
 
         when(mDeviceStateToLayoutMapSpy.get(DeviceStateToLayoutMap.STATE_DEFAULT))
                 .thenReturn(threeDevicesEnabledLayout);
@@ -622,15 +635,18 @@
         oneDeviceEnabledLayout.createDisplayLocked(
                 displayAddressOne,
                 /* isDefault= */ true,
-                /* isEnabled= */ true);
+                /* isEnabled= */ true,
+                mIdProducer);
         oneDeviceEnabledLayout.createDisplayLocked(
                 displayAddressTwo,
                 /* isDefault= */ false,
-                /* isEnabled= */ false);
+                /* isEnabled= */ false,
+                mIdProducer);
         oneDeviceEnabledLayout.createDisplayLocked(
                 displayAddressThree,
                 /* isDefault= */ false,
-                /* isEnabled= */ false);
+                /* isEnabled= */ false,
+                mIdProducer);
 
         when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(oneDeviceEnabledLayout);
         when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(threeDevicesEnabledLayout);
diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
index 3b0a22f..35a677e 100644
--- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -344,6 +344,40 @@
         assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f);
     }
 
+    @Test
+    public void testBrightnessInitialisesWithInvalidFloat() {
+        final String uniqueDisplayId = "test:123";
+        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return true;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+
+        // Set any value which initialises Display state
+        float refreshRate = 85.3f;
+        mDataStore.loadIfNeeded();
+        mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate);
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        mTestLooper.dispatchAll();
+        assertTrue(mInjector.wasWriteSuccessful());
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+        assertTrue(Float.isNaN(mDataStore.getBrightness(testDisplayDevice)));
+    }
+
+
     public class TestInjector extends PersistentDataStore.Injector {
         private InputStream mReadStream;
         private OutputStream mWriteStream;
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
index a5d7a10..dcf217c 100644
--- a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
@@ -29,6 +29,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.R;
+import com.android.server.display.brightness.strategy.BoostBrightnessStrategy;
 import com.android.server.display.brightness.strategy.DozeBrightnessStrategy;
 import com.android.server.display.brightness.strategy.InvalidBrightnessStrategy;
 import com.android.server.display.brightness.strategy.OverrideBrightnessStrategy;
@@ -56,6 +57,8 @@
     @Mock
     private TemporaryBrightnessStrategy mTemporaryBrightnessStrategy;
     @Mock
+    private BoostBrightnessStrategy mBoostBrightnessStrategy;
+    @Mock
     private InvalidBrightnessStrategy mInvalidBrightnessStrategy;
     @Mock
     private Context mContext;
@@ -92,6 +95,11 @@
                     }
 
                     @Override
+                    BoostBrightnessStrategy getBoostBrightnessStrategy() {
+                        return mBoostBrightnessStrategy;
+                    }
+
+                    @Override
                     InvalidBrightnessStrategy getInvalidBrightnessStrategy() {
                         return mInvalidBrightnessStrategy;
                     }
@@ -140,6 +148,17 @@
     }
 
     @Test
+    public void selectStrategySelectsBoostStrategyWhenValid() {
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+        displayPowerRequest.boostScreenBrightness = true;
+        displayPowerRequest.screenBrightnessOverride = Float.NaN;
+        when(mTemporaryBrightnessStrategy.getTemporaryScreenBrightness()).thenReturn(Float.NaN);
+        assertEquals(mDisplayBrightnessStrategySelector.selectStrategy(displayPowerRequest,
+                Display.STATE_ON), mBoostBrightnessStrategy);
+    }
+
+    @Test
     public void selectStrategySelectsInvalidStrategyWhenNoStrategyIsValid() {
         DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
                 DisplayManagerInternal.DisplayPowerRequest.class);
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java
new file mode 100644
index 0000000..431a239
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.display.brightness.strategy;
+
+
+import static org.junit.Assert.assertEquals;
+
+import android.hardware.display.DisplayManagerInternal;
+import android.os.PowerManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.brightness.BrightnessReason;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+
+public class BoostBrightnessStrategyTest {
+    private BoostBrightnessStrategy mBoostBrightnessStrategy;
+
+    @Before
+    public void before() {
+        mBoostBrightnessStrategy = new BoostBrightnessStrategy();
+    }
+
+    @Test
+    public void updateBrightnessWorksAsExpectedWhenBoostBrightnessIsRequested() {
+        DisplayManagerInternal.DisplayPowerRequest
+                displayPowerRequest = new DisplayManagerInternal.DisplayPowerRequest();
+        displayPowerRequest.boostScreenBrightness = true;
+        BrightnessReason brightnessReason = new BrightnessReason();
+        brightnessReason.setReason(BrightnessReason.REASON_BOOST);
+        DisplayBrightnessState expectedDisplayBrightnessState =
+                new DisplayBrightnessState.Builder()
+                        .setBrightness(PowerManager.BRIGHTNESS_MAX)
+                        .setBrightnessReason(brightnessReason)
+                        .setSdrBrightness(PowerManager.BRIGHTNESS_MAX)
+                        .build();
+        DisplayBrightnessState updatedDisplayBrightnessState =
+                mBoostBrightnessStrategy.updateBrightness(displayPowerRequest);
+        assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
+    }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index 09cd47a..2cb46da 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -24,6 +24,8 @@
 
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.media.AudioManager;
 import android.os.Looper;
@@ -52,6 +54,7 @@
     private Context mContextSpy;
     private HdmiCecLocalDeviceAudioSystem mHdmiCecLocalDeviceAudioSystem;
     private FakePowerManagerWrapper mPowerManager;
+    private TestCallback mCallback;
     private ArcTerminationActionFromAvr mAction;
 
     private FakeNativeWrapper mNativeWrapper;
@@ -112,7 +115,9 @@
             }
         };
         mHdmiCecLocalDeviceAudioSystem.init();
-        mAction = new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem);
+        mCallback = new TestCallback();
+        mAction = new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem,
+                mCallback);
 
         mLocalDevices.add(mHdmiCecLocalDeviceAudioSystem);
         hdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
@@ -121,6 +126,20 @@
         mTestLooper.dispatchAll();
     }
 
+    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 testSendMessage_sendFailed() {
         mNativeWrapper.setMessageSendResult(Constants.MESSAGE_TERMINATE_ARC,
@@ -133,6 +152,7 @@
         assertThat(mNativeWrapper.getResultMessages()).contains(terminateArc);
 
         assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
+        assertThat(mCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
     }
 
     @Test
@@ -149,6 +169,7 @@
         mTestLooper.dispatchAll();
 
         assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
+        assertThat(mCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_TIMEOUT);
     }
 
     @Test
@@ -167,5 +188,28 @@
         mTestLooper.dispatchAll();
 
         assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
+        assertThat(mCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
+    }
+
+    @Test
+    public void testReportArcTerminated_featureAbort() {
+        mHdmiCecLocalDeviceAudioSystem.addAndStartAction(mAction);
+        mTestLooper.dispatchAll();
+        HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+                Constants.ADDR_AUDIO_SYSTEM, Constants.ADDR_TV);
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(terminateArc);
+
+        HdmiCecMessage arcTerminatedResponse = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                Constants.ADDR_TV,
+                Constants.ADDR_AUDIO_SYSTEM,
+                Constants.MESSAGE_TERMINATE_ARC,
+                Constants.ABORT_REFUSED);
+
+        mNativeWrapper.onCecMessage(arcTerminatedResponse);
+        mTestLooper.dispatchAll();
+
+        assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
+        assertThat(mCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 7c6c990..de2c218 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -535,6 +535,25 @@
     }
 
     @Test
+    public void handleRequestArcTerminate_callbackIsPreserved() throws Exception {
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceAudioSystem.setArcStatus(true);
+        assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isTrue();
+        mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+                new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem, callback));
+
+        HdmiCecMessage message =
+                HdmiCecMessageBuilder.buildRequestArcTermination(ADDR_TV, ADDR_AUDIO_SYSTEM);
+        assertThat(mHdmiCecLocalDeviceAudioSystem.handleRequestArcTermination(message))
+                .isEqualTo(Constants.HANDLED);
+
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDeviceAudioSystem.getActions(
+                ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0)).isEqualTo(callback);
+    }
+
+    @Test
     public void handleRequestArcInit_arcIsNotSupported() throws Exception {
         HdmiCecMessage message =
                 HdmiCecMessageBuilder.buildRequestArcInitiation(ADDR_TV, ADDR_AUDIO_SYSTEM);
@@ -880,4 +899,13 @@
         assertThat(mNativeWrapper.getResultMessages()).doesNotContain(
                 systemAudioModeRequest_fromAudioSystem);
     }
+
+    private static class TestCallback extends IHdmiControlCallback.Stub {
+        private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();
+
+        @Override
+        public void onComplete(int result) {
+            mCallbackResult.add(result);
+        }
+    }
 }
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 ef2b212..49a0a9a52 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -1036,6 +1036,7 @@
 
     @Test
     public void setSoundbarMode_enabled_addAudioSystemLocalDevice() {
+        mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
         // Initialize the local devices excluding the audio system.
         mHdmiControlServiceSpy.clearCecLocalDevices();
         mLocalDevices.remove(mAudioSystemDeviceSpy);
@@ -1053,6 +1054,7 @@
 
     @Test
     public void setSoundbarMode_disabled_removeAudioSystemLocalDevice() {
+        mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
         // Initialize the local devices excluding the audio system.
         mHdmiControlServiceSpy.clearCecLocalDevices();
         mLocalDevices.remove(mAudioSystemDeviceSpy);
@@ -1073,6 +1075,10 @@
                 HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
                 HdmiControlManager.SOUNDBAR_MODE_DISABLED);
         mTestLooper.dispatchAll();
+
+        // Wait for ArcTerminationActionFromAvr timeout for the logical address allocation to start.
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
         assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
     }
 
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 dc47b5e..0589b3a 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -22,6 +23,7 @@
 import android.os.PersistableBundle;
 import android.os.SystemClock;
 import android.test.RenamingDelegatingContext;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 
@@ -38,9 +40,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
 import java.time.Clock;
 import java.time.ZoneOffset;
-import java.util.Iterator;
 
 /**
  * Test reading and writing correctly from file.
@@ -93,11 +95,147 @@
         mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L);
     }
 
+    private void setUseSplitFiles(boolean useSplitFiles) throws Exception {
+        mTaskStoreUnderTest.setUseSplitFiles(useSplitFiles);
+        waitForPendingIo();
+    }
+
     private void waitForPendingIo() throws Exception {
         assertTrue("Timed out waiting for persistence I/O to complete",
                 mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L));
     }
 
+    /** Test that we properly remove the last job of an app from the persisted file. */
+    @Test
+    public void testRemovingLastJob_singleFile() throws Exception {
+        setUseSplitFiles(false);
+        runRemovingLastJob();
+    }
+
+    /** Test that we properly remove the last job of an app from the persisted file. */
+    @Test
+    public void testRemovingLastJob_splitFiles() throws Exception {
+        setUseSplitFiles(true);
+        runRemovingLastJob();
+    }
+
+    private void runRemovingLastJob() throws Exception {
+        final JobInfo task1 = new Builder(8, mComponent)
+                .setRequiresDeviceIdle(true)
+                .setPeriodic(10000L)
+                .setRequiresCharging(true)
+                .setPersisted(true)
+                .build();
+        final JobInfo task2 = new Builder(12, mComponent)
+                .setMinimumLatency(5000L)
+                .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
+                .setOverrideDeadline(30000L)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+                .setPersisted(true)
+                .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);
+        runWritingJobsToDisk(JobStatus1, JobStatus2);
+
+        // Remove 1 job
+        mTaskStoreUnderTest.remove(JobStatus1, true);
+        waitForPendingIo();
+        JobSet jobStatusSet = new JobSet();
+        mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+        assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
+        JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+
+        assertJobsEqual(JobStatus2, loaded);
+        assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(JobStatus2));
+
+        // Remove 2nd job
+        mTaskStoreUnderTest.remove(JobStatus2, true);
+        waitForPendingIo();
+        jobStatusSet = new JobSet();
+        mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+        assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size());
+    }
+
+    /** Test that we properly clear the persisted file when all jobs are dropped. */
+    @Test
+    public void testClearJobs_singleFile() throws Exception {
+        setUseSplitFiles(false);
+        runClearJobs();
+    }
+
+    /** Test that we properly clear the persisted file when all jobs are dropped. */
+    @Test
+    public void testClearJobs_splitFiles() throws Exception {
+        setUseSplitFiles(true);
+        runClearJobs();
+    }
+
+    private void runClearJobs() throws Exception {
+        final JobInfo task1 = new Builder(8, mComponent)
+                .setRequiresDeviceIdle(true)
+                .setPeriodic(10000L)
+                .setRequiresCharging(true)
+                .setPersisted(true)
+                .build();
+        final JobInfo task2 = new Builder(12, mComponent)
+                .setMinimumLatency(5000L)
+                .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
+                .setOverrideDeadline(30000L)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+                .setPersisted(true)
+                .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);
+        runWritingJobsToDisk(JobStatus1, JobStatus2);
+
+        // Remove all jobs
+        mTaskStoreUnderTest.clear();
+        waitForPendingIo();
+        JobSet jobStatusSet = new JobSet();
+        mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+        assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size());
+    }
+
+    @Test
+    public void testExtractUidFromJobFileName() {
+        File file = new File(mTestContext.getFilesDir(), "randomName");
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), "jobs.xml");
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), ".xml");
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), "1000.xml");
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), "10000");
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX);
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "text.xml");
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + ".xml");
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "-10123.xml");
+        assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "1.xml");
+        assertEquals(1, JobStore.extractUidFromJobFileName(file));
+
+        file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "101023.xml");
+        assertEquals(101023, JobStore.extractUidFromJobFileName(file));
+    }
+
     @Test
     public void testStringToIntArrayAndIntArrayToString() {
         final int[] netCapabilitiesIntArray = { 1, 3, 5, 7, 9 };
@@ -144,13 +282,13 @@
 
     @Test
     public void testWritingTwoJobsToDisk_singleFile() throws Exception {
-        mTaskStoreUnderTest.setUseSplitFiles(false);
+        setUseSplitFiles(false);
         runWritingTwoJobsToDisk();
     }
 
     @Test
     public void testWritingTwoJobsToDisk_splitFiles() throws Exception {
-        mTaskStoreUnderTest.setUseSplitFiles(true);
+        setUseSplitFiles(true);
         runWritingTwoJobsToDisk();
     }
 
@@ -172,28 +310,44 @@
         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);
-        mTaskStoreUnderTest.add(taskStatus1);
-        mTaskStoreUnderTest.add(taskStatus2);
+
+        runWritingJobsToDisk(taskStatus1, taskStatus2);
+    }
+
+    private void runWritingJobsToDisk(JobStatus... jobStatuses) throws Exception {
+        ArraySet<JobStatus> expectedJobs = new ArraySet<>();
+        for (JobStatus jobStatus : jobStatuses) {
+            mTaskStoreUnderTest.add(jobStatus);
+            expectedJobs.add(jobStatus);
+        }
         waitForPendingIo();
 
         final JobSet jobStatusSet = new JobSet();
         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
-        assertEquals("Incorrect # of persisted tasks.", 2, jobStatusSet.size());
-        Iterator<JobStatus> it = jobStatusSet.getAllJobs().iterator();
-        JobStatus loaded1 = it.next();
-        JobStatus loaded2 = it.next();
+        assertEquals("Incorrect # of persisted tasks.", expectedJobs.size(), jobStatusSet.size());
+        int count = 0;
+        final int expectedCount = expectedJobs.size();
+        for (JobStatus loaded : jobStatusSet.getAllJobs()) {
+            count++;
+            for (int i = 0; i < expectedJobs.size(); ++i) {
+                JobStatus expected = expectedJobs.valueAt(i);
 
-        // Reverse them so we know which comparison to make.
-        if (loaded1.getJobId() != 8) {
-            JobStatus tmp = loaded1;
-            loaded1 = loaded2;
-            loaded2 = tmp;
+                try {
+                    assertJobsEqual(expected, loaded);
+                    expectedJobs.remove(expected);
+                    break;
+                } catch (AssertionError e) {
+                    // Not equal. Move along.
+                }
+            }
         }
-
-        assertJobsEqual(taskStatus1, loaded1);
-        assertJobsEqual(taskStatus2, loaded2);
-        assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus1));
-        assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus2));
+        assertEquals("Loaded more jobs than expected", expectedCount, count);
+        if (expectedJobs.size() > 0) {
+            fail("Not all expected jobs were restored");
+        }
+        for (JobStatus jobStatus : jobStatuses) {
+            assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(jobStatus));
+        }
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index c934e65..15eaf5d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -33,22 +33,27 @@
 import android.app.admin.DeviceStateCache;
 import android.app.trust.TrustManager;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
-import android.hardware.authsecret.V1_0.IAuthSecret;
+import android.hardware.authsecret.IAuthSecret;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.FileUtils;
 import android.os.IProgressListener;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.IStorageManager;
 import android.os.storage.StorageManager;
+import android.provider.Settings;
 import android.security.KeyStore;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.internal.util.test.FakeSettingsProviderRule;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.LockSettingsInternal;
 import com.android.internal.widget.LockscreenCredential;
@@ -59,6 +64,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.runner.RunWith;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
@@ -106,7 +112,8 @@
     FingerprintManager mFingerprintManager;
     FaceManager mFaceManager;
     PackageManager mPackageManager;
-    FakeSettings mSettings;
+    @Rule
+    public FakeSettingsProviderRule mSettingsRule = FakeSettingsProvider.rule();
 
     @Before
     public void setUp_baseServices() throws Exception {
@@ -126,7 +133,6 @@
         mFingerprintManager = mock(FingerprintManager.class);
         mFaceManager = mock(FaceManager.class);
         mPackageManager = mock(PackageManager.class);
-        mSettings = new FakeSettings();
 
         LocalServices.removeServiceForTest(LockSettingsInternal.class);
         LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
@@ -134,12 +140,13 @@
         LocalServices.addService(DevicePolicyManagerInternal.class, mDevicePolicyManagerInternal);
         LocalServices.addService(WindowManagerInternal.class, mMockWindowManager);
 
-        mContext = new MockLockSettingsContext(InstrumentationRegistry.getContext(), mUserManager,
-                mNotificationManager, mDevicePolicyManager, mock(StorageManager.class),
-                mock(TrustManager.class), mock(KeyguardManager.class), mFingerprintManager,
-                mFaceManager, mPackageManager);
+        final Context origContext = InstrumentationRegistry.getContext();
+        mContext = new MockLockSettingsContext(origContext,
+                mSettingsRule.mockContentResolver(origContext), mUserManager, mNotificationManager,
+                mDevicePolicyManager, mock(StorageManager.class), mock(TrustManager.class),
+                mock(KeyguardManager.class), mFingerprintManager, mFaceManager, mPackageManager);
         mStorage = new LockSettingsStorageTestable(mContext,
-                new File(InstrumentationRegistry.getContext().getFilesDir(), "locksettings"));
+                new File(origContext.getFilesDir(), "locksettings"));
         File storageDir = mStorage.mStorageDir;
         if (storageDir.exists()) {
             FileUtils.deleteContents(storageDir);
@@ -153,7 +160,7 @@
         mService = new LockSettingsServiceTestable(mContext, mStorage,
                 mGateKeeperService, mKeyStore, setUpStorageManagerMock(), mActivityManager,
                 mSpManager, mAuthSecretService, mGsiService, mRecoverableKeyStoreManager,
-                mUserManagerInternal, mDeviceStateCache, mSettings);
+                mUserManagerInternal, mDeviceStateCache);
         mService.mHasSecureLockScreen = true;
         when(mUserManager.getUserInfo(eq(PRIMARY_USER_ID))).thenReturn(PRIMARY_USER_INFO);
         mPrimaryUserProfiles.add(PRIMARY_USER_INFO);
@@ -186,10 +193,25 @@
         mockBiometricsHardwareFingerprintsAndTemplates(PRIMARY_USER_ID);
         mockBiometricsHardwareFingerprintsAndTemplates(MANAGED_PROFILE_USER_ID);
 
-        mSettings.setDeviceProvisioned(true);
+        setDeviceProvisioned(true);
         mLocalService = LocalServices.getService(LockSettingsInternal.class);
     }
 
+    protected void setDeviceProvisioned(boolean provisioned) {
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.DEVICE_PROVISIONED, provisioned ? 1 : 0);
+    }
+
+    protected void setUserSetupComplete(boolean complete) {
+        Settings.Secure.putIntForUser(mContext.getContentResolver(),
+                Settings.Secure.USER_SETUP_COMPLETE, complete ? 1 : 0, UserHandle.USER_SYSTEM);
+    }
+
+    protected void setSecureFrpMode(boolean secure) {
+        Settings.Secure.putIntForUser(mContext.getContentResolver(),
+                Settings.Secure.SECURE_FRP_MODE, secure ? 1 : 0, UserHandle.USER_SYSTEM);
+    }
+
     private UserInfo installChildProfile(int profileId) {
         final UserInfo userInfo = new UserInfo(
             profileId, null, null, UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_MANAGED_PROFILE);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/FakeSettings.java b/services/tests/servicestests/src/com/android/server/locksettings/FakeSettings.java
deleted file mode 100644
index 2bcd653..0000000
--- a/services/tests/servicestests/src/com/android/server/locksettings/FakeSettings.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.locksettings;
-
-import android.content.ContentResolver;
-import android.os.UserHandle;
-import android.provider.Settings;
-
-public class FakeSettings {
-
-    private int mDeviceProvisioned;
-    private int mSecureFrpMode;
-    private int mUserSetupComplete;
-
-    public void setDeviceProvisioned(boolean provisioned) {
-        mDeviceProvisioned = provisioned ? 1 : 0;
-    }
-
-    public void setSecureFrpMode(boolean secure) {
-        mSecureFrpMode = secure ? 1 : 0;
-    }
-
-    public void setUserSetupComplete(boolean complete) {
-        mUserSetupComplete = complete ? 1 : 0;
-    }
-
-    public int globalGetInt(String keyName) {
-        switch (keyName) {
-            case Settings.Global.DEVICE_PROVISIONED:
-                return mDeviceProvisioned;
-            default:
-                throw new IllegalArgumentException("Unhandled global settings: " + keyName);
-        }
-    }
-
-    public int secureGetInt(ContentResolver contentResolver, String keyName, int defaultValue,
-            int userId) {
-        if (Settings.Secure.SECURE_FRP_MODE.equals(keyName) && userId == UserHandle.USER_SYSTEM) {
-            return mSecureFrpMode;
-        }
-        if (Settings.Secure.USER_SETUP_COMPLETE.equals(keyName)
-                && userId == UserHandle.USER_SYSTEM) {
-            return mUserSetupComplete;
-        }
-        return defaultValue;
-    }
-}
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 85db23c..d3b647d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -20,10 +20,9 @@
 
 import android.app.IActivityManager;
 import android.app.admin.DeviceStateCache;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.UserInfo;
-import android.hardware.authsecret.V1_0.IAuthSecret;
+import android.hardware.authsecret.IAuthSecret;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.Process;
@@ -52,14 +51,12 @@
         private RecoverableKeyStoreManager mRecoverableKeyStoreManager;
         private UserManagerInternal mUserManagerInternal;
         private DeviceStateCache mDeviceStateCache;
-        private FakeSettings mSettings;
 
         public MockInjector(Context context, LockSettingsStorage storage, KeyStore keyStore,
                 IActivityManager activityManager,
                 IStorageManager storageManager, SyntheticPasswordManager spManager,
                 FakeGsiService gsiService, RecoverableKeyStoreManager recoverableKeyStoreManager,
-                UserManagerInternal userManagerInternal, DeviceStateCache deviceStateCache,
-                FakeSettings settings) {
+                UserManagerInternal userManagerInternal, DeviceStateCache deviceStateCache) {
             super(context);
             mLockSettingsStorage = storage;
             mKeyStore = keyStore;
@@ -70,7 +67,6 @@
             mRecoverableKeyStoreManager = recoverableKeyStoreManager;
             mUserManagerInternal = userManagerInternal;
             mDeviceStateCache = deviceStateCache;
-            mSettings = settings;
         }
 
         @Override
@@ -119,18 +115,6 @@
         }
 
         @Override
-        public int settingsGlobalGetInt(ContentResolver contentResolver, String keyName,
-                int defaultValue) {
-            return mSettings.globalGetInt(keyName);
-        }
-
-        @Override
-        public int settingsSecureGetInt(ContentResolver contentResolver, String keyName,
-                int defaultValue, int userId) {
-            return mSettings.secureGetInt(contentResolver, keyName, defaultValue, userId);
-        }
-
-        @Override
         public UserManagerInternal getUserManagerInternal() {
             return mUserManagerInternal;
         }
@@ -165,13 +149,12 @@
             IStorageManager storageManager, IActivityManager mActivityManager,
             SyntheticPasswordManager spManager, IAuthSecret authSecretService,
             FakeGsiService gsiService, RecoverableKeyStoreManager recoverableKeyStoreManager,
-            UserManagerInternal userManagerInternal, DeviceStateCache deviceStateCache,
-            FakeSettings settings) {
+            UserManagerInternal userManagerInternal, DeviceStateCache deviceStateCache) {
         super(new MockInjector(context, storage, keystore, mActivityManager,
-                storageManager, spManager, gsiService,
-                recoverableKeyStoreManager, userManagerInternal, deviceStateCache, settings));
+                storageManager, spManager, gsiService, recoverableKeyStoreManager,
+                userManagerInternal, deviceStateCache));
         mGateKeeperService = gatekeeper;
-        mAuthSecretService = authSecretService;
+        mAuthSecretServiceAidl = authSecretService;
     }
 
     @Override
@@ -216,4 +199,4 @@
         UserInfo userInfo = mUserManager.getUserInfo(userId);
         return userInfo.isCloneProfile() || userInfo.isManagedProfile();
     }
-}
\ No newline at end of file
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index 3f259e3..196226a 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -424,8 +424,8 @@
 
     @Test
     public void testCredentialChangeNotPossibleInSecureFrpModeDuringSuw() {
-        mSettings.setUserSetupComplete(false);
-        mSettings.setSecureFrpMode(true);
+        setUserSetupComplete(false);
+        setSecureFrpMode(true);
         try {
             mService.setLockCredential(newPassword("1234"), nonePassword(), PRIMARY_USER_ID);
             fail("Password shouldn't be changeable before FRP unlock");
@@ -434,8 +434,8 @@
 
     @Test
     public void testCredentialChangePossibleInSecureFrpModeAfterSuw() {
-        mSettings.setUserSetupComplete(true);
-        mSettings.setSecureFrpMode(true);
+        setUserSetupComplete(true);
+        setSecureFrpMode(true);
         assertTrue(mService.setLockCredential(newPassword("1234"), nonePassword(),
                 PRIMARY_USER_ID));
     }
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 a663858..95d0e15 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
@@ -31,6 +31,7 @@
 import android.app.NotificationManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.TrustManager;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.database.sqlite.SQLiteDatabase;
@@ -49,11 +50,14 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.internal.util.test.FakeSettingsProviderRule;
 import com.android.server.PersistentDataBlockManagerInternal;
 import com.android.server.locksettings.LockSettingsStorage.PersistentData;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -78,14 +82,17 @@
 
     public static final byte[] PAYLOAD = new byte[] {1, 2, -1, -2, 33};
 
-    LockSettingsStorageTestable mStorage;
-    File mStorageDir;
-
+    private LockSettingsStorageTestable mStorage;
+    private File mStorageDir;
     private File mDb;
+    @Rule
+    public FakeSettingsProviderRule mSettingsRule = FakeSettingsProvider.rule();
 
     @Before
     public void setUp() throws Exception {
-        mStorageDir = new File(InstrumentationRegistry.getContext().getFilesDir(), "locksettings");
+        final Context origContext = InstrumentationRegistry.getContext();
+
+        mStorageDir = new File(origContext.getFilesDir(), "locksettings");
         mDb = InstrumentationRegistry.getContext().getDatabasePath("locksettings.db");
 
         assertTrue(mStorageDir.exists() || mStorageDir.mkdirs());
@@ -98,8 +105,8 @@
         // User 3 is a profile of user 0.
         when(mockUserManager.getProfileParent(eq(3))).thenReturn(new UserInfo(0, "name", 0));
 
-        MockLockSettingsContext context = new MockLockSettingsContext(
-                InstrumentationRegistry.getContext(), mockUserManager,
+        MockLockSettingsContext context = new MockLockSettingsContext(origContext,
+                mSettingsRule.mockContentResolver(origContext), mockUserManager,
                 mock(NotificationManager.class), mock(DevicePolicyManager.class),
                 mock(StorageManager.class), mock(TrustManager.class), mock(KeyguardManager.class),
                 mock(FingerprintManager.class), mock(FaceManager.class),
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
index c2f94e2..fc0ca7e 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
@@ -44,7 +44,7 @@
     @Before
     public void setDeviceNotProvisioned() throws Exception {
         // FRP credential can only be verified prior to provisioning
-        mSettings.setDeviceProvisioned(false);
+        setDeviceProvisioned(false);
     }
 
     @Before
@@ -98,6 +98,7 @@
         mService.setLockCredential(newPassword("1234"), nonePassword(), PRIMARY_USER_ID);
         assertEquals(CREDENTIAL_TYPE_PASSWORD, mService.getCredentialType(USER_FRP));
 
+        setDeviceProvisioned(true);
         mService.setLockCredential(nonePassword(), newPassword("1234"), PRIMARY_USER_ID);
         assertEquals(CREDENTIAL_TYPE_NONE, mService.getCredentialType(USER_FRP));
     }
@@ -106,7 +107,7 @@
     public void testFrpCredential_cannotVerifyAfterProvsioning() {
         mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
 
-        mSettings.setDeviceProvisioned(true);
+        setDeviceProvisioned(true);
         assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
                 mService.verifyCredential(newPin("1234"), USER_FRP, 0 /* flags */)
                         .getResponseCode());
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/MockLockSettingsContext.java b/services/tests/servicestests/src/com/android/server/locksettings/MockLockSettingsContext.java
index efa1b04..21c367b 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/MockLockSettingsContext.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/MockLockSettingsContext.java
@@ -21,6 +21,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
@@ -35,22 +36,25 @@
 
 public class MockLockSettingsContext extends ContextWrapper {
 
-    private UserManager mUserManager;
-    private NotificationManager mNotificationManager;
-    private DevicePolicyManager mDevicePolicyManager;
-    private StorageManager mStorageManager;
-    private TrustManager mTrustManager;
-    private KeyguardManager mKeyguardManager;
-    private FingerprintManager mFingerprintManager;
-    private FaceManager mFaceManager;
-    private PackageManager mPackageManager;
+    private final ContentResolver mContentResolver;
+    private final UserManager mUserManager;
+    private final NotificationManager mNotificationManager;
+    private final DevicePolicyManager mDevicePolicyManager;
+    private final StorageManager mStorageManager;
+    private final TrustManager mTrustManager;
+    private final KeyguardManager mKeyguardManager;
+    private final FingerprintManager mFingerprintManager;
+    private final FaceManager mFaceManager;
+    private final PackageManager mPackageManager;
 
-    public MockLockSettingsContext(Context base, UserManager userManager,
-            NotificationManager notificationManager, DevicePolicyManager devicePolicyManager,
-            StorageManager storageManager, TrustManager trustManager,
-            KeyguardManager keyguardManager, FingerprintManager fingerprintManager,
-            FaceManager faceManager, PackageManager packageManager) {
+    public MockLockSettingsContext(Context base, ContentResolver contentResolver,
+            UserManager userManager, NotificationManager notificationManager,
+            DevicePolicyManager devicePolicyManager, StorageManager storageManager,
+            TrustManager trustManager, KeyguardManager keyguardManager,
+            FingerprintManager fingerprintManager, FaceManager faceManager,
+            PackageManager packageManager) {
         super(base);
+        mContentResolver = contentResolver;
         mUserManager = userManager;
         mNotificationManager = notificationManager;
         mDevicePolicyManager = devicePolicyManager;
@@ -63,6 +67,11 @@
     }
 
     @Override
+    public ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    @Override
     public Object getSystemService(String name) {
         if (USER_SERVICE.equals(name)) {
             return mUserManager;
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 3f4bec6..a441824 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -59,6 +59,7 @@
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 
 
 /**
@@ -136,6 +137,43 @@
         assertTrue(mService.isSyntheticPasswordBasedCredential(userId));
     }
 
+    protected void initializeSyntheticPassword(int userId) {
+        synchronized (mService.mSpManager) {
+            mService.initializeSyntheticPasswordLocked(userId);
+        }
+    }
+
+    // Tests that the FRP credential is updated when an LSKF-based protector is created for the user
+    // that owns the FRP credential, if the device is already provisioned.
+    @Test
+    public void testFrpCredentialSyncedIfDeviceProvisioned() throws RemoteException {
+        setDeviceProvisioned(true);
+        initializeSyntheticPassword(PRIMARY_USER_ID);
+        verify(mStorage.mPersistentDataBlockManager).setFrpCredentialHandle(any());
+    }
+
+    // Tests that the FRP credential is not updated when an LSKF-based protector is created for the
+    // user that owns the FRP credential, if the new credential is empty and the device is not yet
+    // provisioned.
+    @Test
+    public void testEmptyFrpCredentialNotSyncedIfDeviceNotProvisioned() throws RemoteException {
+        setDeviceProvisioned(false);
+        initializeSyntheticPassword(PRIMARY_USER_ID);
+        verify(mStorage.mPersistentDataBlockManager, never()).setFrpCredentialHandle(any());
+    }
+
+    // Tests that the FRP credential is updated when an LSKF-based protector is created for the user
+    // that owns the FRP credential, if the new credential is nonempty and the device is not yet
+    // provisioned.
+    @Test
+    public void testNonEmptyFrpCredentialSyncedIfDeviceNotProvisioned() throws RemoteException {
+        setDeviceProvisioned(false);
+        initializeSyntheticPassword(PRIMARY_USER_ID);
+        verify(mStorage.mPersistentDataBlockManager, never()).setFrpCredentialHandle(any());
+        mService.setLockCredential(newPassword("password"), nonePassword(), PRIMARY_USER_ID);
+        verify(mStorage.mPersistentDataBlockManager).setFrpCredentialHandle(any());
+    }
+
     @Test
     public void testChangeCredential() throws RemoteException {
         final LockscreenCredential password = newPassword("password");
@@ -192,9 +230,11 @@
                 badPassword, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
 
         // Check the same secret was passed each time
-        ArgumentCaptor<ArrayList<Byte>> secret = ArgumentCaptor.forClass(ArrayList.class);
-        verify(mAuthSecretService, atLeastOnce()).primaryUserCredential(secret.capture());
-        assertEquals(1, secret.getAllValues().stream().distinct().count());
+        ArgumentCaptor<byte[]> secret = ArgumentCaptor.forClass(byte[].class);
+        verify(mAuthSecretService, atLeastOnce()).setPrimaryUserCredential(secret.capture());
+        for (byte[] val : secret.getAllValues()) {
+          assertArrayEquals(val, secret.getAllValues().get(0));
+        }
     }
 
     @Test
@@ -205,7 +245,7 @@
         reset(mAuthSecretService);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
-        verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class));
+        verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
     }
 
     @Test
@@ -215,7 +255,7 @@
         initializeCredential(password, SECONDARY_USER_ID);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 password, SECONDARY_USER_ID, 0 /* flags */).getResponseCode());
-        verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
+        verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class));
     }
 
     @Test
@@ -226,7 +266,7 @@
 
         reset(mAuthSecretService);
         mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
-        verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class));
+        verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
     }
 
     @Test
@@ -554,7 +594,7 @@
         initializeCredential(password, PRIMARY_USER_ID);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
-        verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
+        verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
index a3ac515..6c13a6f 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
@@ -1,11 +1,17 @@
 package com.android.server.locksettings;
 
+import static org.junit.Assert.assertEquals;
+
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+import com.google.android.collect.Sets;
+
 import org.junit.Before;
+import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @SmallTest
@@ -17,4 +23,36 @@
     public void enableWeaver() throws Exception {
         mSpManager.enableWeaver();
     }
+
+    // Tests that if the device is not yet provisioned and the FRP credential uses Weaver, then the
+    // Weaver slot of the FRP credential is not reused.  Assumes that Weaver slots are allocated
+    // sequentially, starting at slot 0.
+    @Test
+    public void testFrpWeaverSlotNotReused() {
+        final int userId = 10;
+        final int frpWeaverSlot = 0;
+
+        setDeviceProvisioned(false);
+        assertEquals(Sets.newHashSet(), mPasswordSlotManager.getUsedSlots());
+        mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, frpWeaverSlot, 0,
+                new byte[1]);
+        initializeSyntheticPassword(userId); // This should allocate a Weaver slot.
+        assertEquals(Sets.newHashSet(1), mPasswordSlotManager.getUsedSlots());
+    }
+
+    // Tests that if the device is already provisioned and the FRP credential uses Weaver, then the
+    // Weaver slot of the FRP credential is reused.  This is not a very interesting test by itself;
+    // it's here as a control for testFrpWeaverSlotNotReused().
+    @Test
+    public void testFrpWeaverSlotReused() {
+        final int userId = 10;
+        final int frpWeaverSlot = 0;
+
+        setDeviceProvisioned(true);
+        assertEquals(Sets.newHashSet(), mPasswordSlotManager.getUsedSlots());
+        mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, frpWeaverSlot, 0,
+                new byte[1]);
+        initializeSyntheticPassword(userId); // This should allocate a Weaver slot.
+        assertEquals(Sets.newHashSet(0), mPasswordSlotManager.getUsedSlots());
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
index ab52928..578b888 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
@@ -323,4 +323,19 @@
         FakeIdmapDaemon.IdmapHeader idmap = idmapd.getIdmap(overlayPath);
         assertEquals(0, CONFIG_SIGNATURE & idmap.policies);
     }
+
+    @Test
+    public void testOnTargetSystemPackageUninstall() throws Exception {
+        installAndAssert(target(TARGET), USER,
+                Set.of(UserPackage.of(USER, TARGET)));
+        installAndAssert(overlay(OVERLAY, TARGET), USER,
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
+        upgradeAndAssert(target(TARGET), USER,
+                Set.of(UserPackage.of(USER, TARGET)),
+                Set.of(UserPackage.of(USER, TARGET)));
+
+        downgradeAndAssert(target(TARGET), USER,
+                Set.of(UserPackage.of(USER, TARGET)),
+                Set.of(UserPackage.of(USER, TARGET)));
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
index bba7669..dab4335 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.NonNull;
+import android.content.Intent;
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
 import android.content.om.OverlayInfo.State;
@@ -160,7 +161,7 @@
      * Adds the package to the device.
      *
      * This corresponds to when the OMS receives the
-     * {@link android.content.Intent#ACTION_PACKAGE_ADDED} broadcast.
+     * {@link Intent#ACTION_PACKAGE_ADDED} broadcast.
      *
      * @throws IllegalStateException if the package is currently installed
      */
@@ -178,10 +179,10 @@
      * Begins upgrading the package.
      *
      * This corresponds to when the OMS receives the
-     * {@link android.content.Intent#ACTION_PACKAGE_REMOVED} broadcast with the
-     * {@link android.content.Intent#EXTRA_REPLACING} extra and then receives the
-     * {@link android.content.Intent#ACTION_PACKAGE_ADDED} broadcast with the
-     * {@link android.content.Intent#EXTRA_REPLACING} extra.
+     * {@link Intent#ACTION_PACKAGE_REMOVED} broadcast with the
+     * {@link Intent#EXTRA_REPLACING} extra and then receives the
+     * {@link Intent#ACTION_PACKAGE_ADDED} broadcast with the
+     * {@link Intent#EXTRA_REPLACING} extra.
      *
      * @throws IllegalStateException if the package is not currently installed
      */
@@ -194,7 +195,35 @@
             throw new IllegalStateException("package " + pkg.packageName + " not installed");
         }
 
-        assertEquals(onReplacingUpdatedPackages, mImpl.onPackageReplacing(pkg.packageName, userId));
+        assertEquals(onReplacingUpdatedPackages, mImpl.onPackageReplacing(pkg.packageName,
+                /* systemUpdateUninstall */ false, userId));
+        mState.add(pkg, userId);
+        assertEquals(onReplacedUpdatedPackages, mImpl.onPackageReplaced(pkg.packageName, userId));
+    }
+
+    /**
+     * Begins downgrading the package. Usually used simulating a system uninstall of its /data
+     * variant.
+     *
+     * This corresponds to when the OMS receives the
+     * {@link Intent#ACTION_PACKAGE_REMOVED} broadcast with the
+     * {@link Intent#EXTRA_REPLACING} and {@link Intent#EXTRA_SYSTEM_UPDATE_UNINSTALL} extras
+     * and then receives the {@link Intent#ACTION_PACKAGE_ADDED} broadcast with the
+     * {@link Intent#EXTRA_REPLACING} extra.
+     *
+     * @throws IllegalStateException if the package is not currently installed
+     */
+    void downgradeAndAssert(FakeDeviceState.PackageBuilder pkg, int userId,
+            @NonNull Set<UserPackage> onReplacingUpdatedPackages,
+            @NonNull Set<UserPackage> onReplacedUpdatedPackages)
+            throws OperationFailedException {
+        final FakeDeviceState.Package replacedPackage = mState.select(pkg.packageName, userId);
+        if (replacedPackage == null) {
+            throw new IllegalStateException("package " + pkg.packageName + " not installed");
+        }
+
+        assertEquals(onReplacingUpdatedPackages, mImpl.onPackageReplacing(pkg.packageName,
+                /* systemUpdateUninstall */ true, userId));
         mState.add(pkg, userId);
         assertEquals(onReplacedUpdatedPackages, mImpl.onPackageReplaced(pkg.packageName, userId));
     }
@@ -203,7 +232,7 @@
      * Removes the package from the device.
      *
      * This corresponds to when the OMS receives the
-     * {@link android.content.Intent#ACTION_PACKAGE_REMOVED} broadcast.
+     * {@link Intent#ACTION_PACKAGE_REMOVED} broadcast.
      *
      * @throws IllegalStateException if the package is not currently installed
      */
diff --git a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
new file mode 100644
index 0000000..c81fbb4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
@@ -0,0 +1,781 @@
+package com.android.server.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.ActivityOptions;
+import android.app.AppOpsManager;
+import android.app.IApplicationThread;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.AttributionSourceState;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.PermissionChecker;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.CrossProfileAppsInternal;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.PermissionInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.permission.PermissionCheckerManager;
+import android.permission.PermissionManager;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+
+import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
+import com.android.internal.util.FunctionalUtils.ThrowingSupplier;
+import com.android.server.LocalServices;
+import com.android.server.pm.permission.PermissionManagerService;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.pm.CrossProfileAppsServiceImplTest
+ */
+@Presubmit
+@RunWith(MockitoJUnitRunner.class)
+public class CrossProfileAppsServiceImplTest {
+    private static final String PACKAGE_ONE = "com.one";
+    private static final String FEATURE_ID = "feature.one";
+    private static final int PACKAGE_ONE_UID = 1111;
+    private static final ComponentName ACTIVITY_COMPONENT =
+            new ComponentName("com.one", "test");
+
+    private static final String PACKAGE_TWO = "com.two";
+    private static final int PACKAGE_TWO_UID = 2222;
+
+    private static final int PRIMARY_USER = 0;
+    private static final int PROFILE_OF_PRIMARY_USER = 10;
+    private static final int SECONDARY_USER = 11;
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private UserManager mUserManager;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private PackageManagerInternal mPackageManagerInternal;
+    @Mock
+    private AppOpsManager mAppOpsManager;
+    @Mock
+    private ActivityManagerInternal mActivityManagerInternal;
+    @Mock
+    private ActivityTaskManagerInternal mActivityTaskManagerInternal;
+    @Mock
+    private IPackageManager mIPackageManager;
+    @Mock
+    private DevicePolicyManagerInternal mDevicePolicyManagerInternal;
+
+    private TestInjector mTestInjector;
+    private ActivityInfo mActivityInfo;
+    private CrossProfileAppsServiceImpl mCrossProfileAppsServiceImpl;
+    private IApplicationThread mIApplicationThread;
+
+    private SparseArray<Boolean> mUserEnabled = new SparseArray<>();
+
+    @Before
+    public void initCrossProfileAppsServiceImpl() {
+        mTestInjector = new TestInjector();
+        LocalServices.removeServiceForTest(CrossProfileAppsInternal.class);
+        mCrossProfileAppsServiceImpl = new CrossProfileAppsServiceImpl(mContext, mTestInjector);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+    }
+
+    @Before
+    public void setupEnabledProfiles() {
+        mUserEnabled.put(PRIMARY_USER, true);
+        mUserEnabled.put(PROFILE_OF_PRIMARY_USER, true);
+        mUserEnabled.put(SECONDARY_USER, true);
+
+        when(mUserManager.getEnabledProfileIds(anyInt())).thenAnswer(
+                invocation -> {
+                    List<Integer> users = new ArrayList<>();
+                    final int targetUser = invocation.getArgument(0);
+                    users.add(targetUser);
+
+                    int profileUserId = -1;
+                    if (targetUser == PRIMARY_USER) {
+                        profileUserId = PROFILE_OF_PRIMARY_USER;
+                    } else if (targetUser == PROFILE_OF_PRIMARY_USER) {
+                        profileUserId = PRIMARY_USER;
+                    }
+
+                    if (profileUserId != -1 && mUserEnabled.get(profileUserId)) {
+                        users.add(profileUserId);
+                    }
+                    return users.stream().mapToInt(i -> i).toArray();
+                });
+    }
+
+    @Before
+    public void setupCaller() {
+        mTestInjector.setCallingUid(PACKAGE_ONE_UID);
+        mTestInjector.setCallingUserId(PRIMARY_USER);
+    }
+
+    @Before
+    public void setupPackage() throws Exception {
+        // PACKAGE_ONE are installed in all users.
+        mockAppsInstalled(PACKAGE_ONE, PRIMARY_USER, true);
+        mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, true);
+        mockAppsInstalled(PACKAGE_ONE, SECONDARY_USER, true);
+
+        // Packages are resolved to their corresponding UID.
+        doAnswer(invocation -> {
+            final int uid = invocation.getArgument(0);
+            final String packageName = invocation.getArgument(1);
+            if (uid == PACKAGE_ONE_UID && PACKAGE_ONE.equals(packageName)) {
+                return null;
+            } else if (uid ==PACKAGE_TWO_UID && PACKAGE_TWO.equals(packageName)) {
+                return null;
+            }
+            throw new SecurityException("Not matching");
+        }).when(mAppOpsManager).checkPackage(anyInt(), anyString());
+
+        // The intent is resolved to the ACTIVITY_COMPONENT.
+        mockActivityLaunchIntentResolvedTo(ACTIVITY_COMPONENT);
+    }
+
+    @Test
+    public void getTargetUserProfiles_fromPrimaryUser_installed() throws Exception {
+        List<UserHandle> targetProfiles =
+                mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+        assertThat(targetProfiles).containsExactly(UserHandle.of(PROFILE_OF_PRIMARY_USER));
+    }
+
+    @Test
+    public void getTargetUserProfiles_fromPrimaryUser_notInstalled() throws Exception {
+        mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
+
+        List<UserHandle> targetProfiles =
+                mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+        assertThat(targetProfiles).isEmpty();
+    }
+
+    @Test
+    public void getTargetUserProfiles_fromPrimaryUser_userNotEnabled() throws Exception {
+        mUserEnabled.put(PROFILE_OF_PRIMARY_USER, false);
+
+        List<UserHandle> targetProfiles =
+                mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+        assertThat(targetProfiles).isEmpty();
+    }
+
+    @Test
+    public void getTargetUserProfiles_fromSecondaryUser() throws Exception {
+        mTestInjector.setCallingUserId(SECONDARY_USER);
+
+        List<UserHandle> targetProfiles =
+                mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+        assertThat(targetProfiles).isEmpty();
+    }
+
+    @Test
+    public void getTargetUserProfiles_fromProfile_installed() throws Exception {
+        mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
+
+        List<UserHandle> targetProfiles =
+                mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+        assertThat(targetProfiles).containsExactly(UserHandle.of(PRIMARY_USER));
+    }
+
+    @Test
+    public void getTargetUserProfiles_fromProfile_notInstalled() throws Exception {
+        mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
+        mockAppsInstalled(PACKAGE_ONE, PRIMARY_USER, false);
+
+        List<UserHandle> targetProfiles =
+                mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
+        assertThat(targetProfiles).isEmpty();
+    }
+
+    @Test(expected = SecurityException.class)
+    public void getTargetUserProfiles_fakeCaller() throws Exception {
+        mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_TWO);
+    }
+
+    @Test
+    public void startActivityAsUser_currentUser() throws Exception {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(PRIMARY_USER).getIdentifier(),
+                                true,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startAnyActivityAsUser_currentUser() {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(PRIMARY_USER).getIdentifier(),
+                                false,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startActivityAsUser_profile_notInstalled() throws Exception {
+        mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
+
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+                                true,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startAnyActivityAsUser_profile_notInstalled() {
+        mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
+
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+                                false,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startActivityAsUser_profile_fakeCaller() throws Exception {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_TWO,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+                                true,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startAnyActivityAsUser_profile_fakeCaller() {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_TWO,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+                                false,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startActivityAsUser_profile_notExported() throws Exception {
+        mActivityInfo.exported = false;
+
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+                                true,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startAnyActivityAsUser_profile_notExported() {
+        try {
+            when(mPackageManager.getPermissionInfo(anyString(), anyInt()))
+                    .thenReturn(new PermissionInfo());
+        } catch (PackageManager.NameNotFoundException ignored) {
+        }
+        mActivityInfo.exported = false;
+
+
+        // There's a bug in static mocking if the APK is large - so here is the next best thing...
+        doReturn(Context.PERMISSION_CHECKER_SERVICE).when(mContext)
+                .getSystemServiceName(PermissionCheckerManager.class);
+        PermissionCheckerManager permissionCheckerManager = mock(PermissionCheckerManager.class);
+        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(permissionCheckerManager)
+                .checkPermission(eq(Manifest.permission.INTERACT_ACROSS_PROFILES), any(
+                        AttributionSourceState.class), anyString(), anyBoolean(), anyBoolean(),
+                        anyBoolean(), anyInt());
+        doReturn(permissionCheckerManager).when(mContext).getSystemService(
+                Context.PERMISSION_CHECKER_SERVICE);
+
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+                                false,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startActivityAsUser_profile_anotherPackage() throws Exception {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                new ComponentName(PACKAGE_TWO, "test"),
+                                UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+                                true,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startAnyActivityAsUser_profile_anotherPackage() {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                new ComponentName(PACKAGE_TWO, "test"),
+                                UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+                                false,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startActivityAsUser_secondaryUser() throws Exception {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(SECONDARY_USER).getIdentifier(),
+                                true,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startAnyActivityAsUser_secondaryUser() {
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                                mIApplicationThread,
+                                PACKAGE_ONE,
+                                FEATURE_ID,
+                                ACTIVITY_COMPONENT,
+                                UserHandle.of(SECONDARY_USER).getIdentifier(),
+                                false,
+                                /* targetTask */ null,
+                                /* options */ null));
+
+        verify(mActivityTaskManagerInternal, never())
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        anyString(),
+                        nullable(String.class),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        anyInt());
+    }
+
+    @Test
+    public void startActivityAsUser_fromProfile_success() throws Exception {
+        mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
+
+        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                mIApplicationThread,
+                PACKAGE_ONE,
+                FEATURE_ID,
+                ACTIVITY_COMPONENT,
+                UserHandle.of(PRIMARY_USER).getIdentifier(),
+                true,
+                /* targetTask */ null,
+                /* options */ null);
+
+        verify(mActivityTaskManagerInternal)
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        eq(PACKAGE_ONE),
+                        eq(FEATURE_ID),
+                        any(Intent.class),
+                        nullable(IBinder.class),
+                        anyInt(),
+                        nullable(Bundle.class),
+                        eq(PRIMARY_USER));
+    }
+
+    @Test
+    public void startActivityAsUser_sameTask_fromProfile_success() throws Exception {
+        mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
+
+        Bundle options = ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle();
+        Binder targetTask = new Binder();
+        mCrossProfileAppsServiceImpl.startActivityAsUser(
+                mIApplicationThread,
+                PACKAGE_ONE,
+                FEATURE_ID,
+                ACTIVITY_COMPONENT,
+                UserHandle.of(PRIMARY_USER).getIdentifier(),
+                true,
+                targetTask,
+                options);
+        verify(mActivityTaskManagerInternal)
+                .startActivityAsUser(
+                        nullable(IApplicationThread.class),
+                        eq(PACKAGE_ONE),
+                        eq(FEATURE_ID),
+                        any(Intent.class),
+                        eq(targetTask),
+                        anyInt(),
+                        eq(options),
+                        eq(PRIMARY_USER));
+    }
+
+    private void mockAppsInstalled(String packageName, int user, boolean installed) {
+        when(mPackageManagerInternal.getPackageInfo(
+                eq(packageName),
+                anyLong(),
+                anyInt(),
+                eq(user)))
+                .thenReturn(installed ? createInstalledPackageInfo() : null);
+    }
+
+    private PackageInfo createInstalledPackageInfo() {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.enabled = true;
+        return packageInfo;
+    }
+
+    private void mockActivityLaunchIntentResolvedTo(ComponentName componentName) {
+        ResolveInfo resolveInfo = new ResolveInfo();
+        ActivityInfo activityInfo = new ActivityInfo();
+        activityInfo.packageName = componentName.getPackageName();
+        activityInfo.name = componentName.getClassName();
+        activityInfo.exported = true;
+        resolveInfo.activityInfo = activityInfo;
+        mActivityInfo = activityInfo;
+
+        when(mPackageManagerInternal.queryIntentActivities(
+                any(Intent.class), nullable(String.class), anyLong(), anyInt(), anyInt()))
+                .thenReturn(Collections.singletonList(resolveInfo));
+    }
+
+    private class TestInjector implements CrossProfileAppsServiceImpl.Injector {
+        private int mCallingUid;
+        private int mCallingUserId;
+        private int mCallingPid;
+
+        public void setCallingUid(int uid) {
+            mCallingUid = uid;
+        }
+
+        public void setCallingPid(int pid) {
+            mCallingPid = pid;
+        }
+
+        public void setCallingUserId(int userId) {
+            mCallingUserId = userId;
+        }
+
+        @Override
+        public int getCallingUid() {
+            return mCallingUid;
+        }
+
+        @Override
+        public int getCallingPid() {
+            return mCallingPid;
+        }
+
+        @Override
+        public int getCallingUserId() {
+            return mCallingUserId;
+        }
+
+        @Override
+        public UserHandle getCallingUserHandle() {
+            return UserHandle.of(mCallingUserId);
+        }
+
+        @Override
+        public long clearCallingIdentity() {
+            return 0;
+        }
+
+        @Override
+        public void restoreCallingIdentity(long token) {
+        }
+
+        @Override
+        public void withCleanCallingIdentity(ThrowingRunnable action) {
+            action.run();
+        }
+
+        @Override
+        public <T> T withCleanCallingIdentity(ThrowingSupplier<T> action) {
+            return action.get();
+        }
+
+        @Override
+        public UserManager getUserManager() {
+            return mUserManager;
+        }
+
+        @Override
+        public PackageManagerInternal getPackageManagerInternal() {
+            return mPackageManagerInternal;
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return mPackageManager;
+        }
+
+        @Override
+        public AppOpsManager getAppOpsManager() {
+            return mAppOpsManager;
+        }
+
+        @Override
+        public ActivityManagerInternal getActivityManagerInternal() {
+            return mActivityManagerInternal;
+        }
+
+        @Override
+        public ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+            return mActivityTaskManagerInternal;
+        }
+
+        @Override
+        public IPackageManager getIPackageManager() {
+            return mIPackageManager;
+        }
+
+        @Override
+        public DevicePolicyManagerInternal getDevicePolicyManagerInternal() {
+            return mDevicePolicyManagerInternal;
+        }
+
+        @Override
+        public void sendBroadcastAsUser(Intent intent, UserHandle user) {
+            mContext.sendBroadcastAsUser(intent, user);
+        }
+
+        @Override
+        public int checkComponentPermission(
+                String permission, int uid, int owningUid, boolean exported) {
+            return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported);
+        }
+
+        @Override
+        public void killUid(int uid) {
+            PermissionManagerService.killUid(
+                    UserHandle.getAppId(uid),
+                    UserHandle.getUserId(uid),
+                    PermissionManager.KILL_APP_REASON_PERMISSIONS_REVOKED);
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 1815e2a..7c1962c 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -21,6 +21,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
+import android.annotation.XmlRes;
 import android.content.Context;
 import android.net.NetworkStats;
 import android.os.BatteryConsumer;
@@ -50,6 +51,7 @@
                     .powerProfileModeledOnly()
                     .includePowerModels()
                     .build();
+    private final Context mContext;
 
     private final PowerProfile mPowerProfile;
     private final MockClock mMockClock = new MockClock();
@@ -67,14 +69,19 @@
     }
 
     public BatteryUsageStatsRule(long currentTime, File historyDir) {
-        Context context = InstrumentationRegistry.getContext();
-        mPowerProfile = spy(new PowerProfile(context, true /* forTest */));
+        mContext = InstrumentationRegistry.getContext();
+        mPowerProfile = spy(new PowerProfile(mContext, true /* forTest */));
         mMockClock.currentTime = currentTime;
         mBatteryStats = new MockBatteryStatsImpl(mMockClock, historyDir);
         mBatteryStats.setPowerProfile(mPowerProfile);
         mBatteryStats.onSystemReady();
     }
 
+    public BatteryUsageStatsRule setTestPowerProfile(@XmlRes int xmlId) {
+        mPowerProfile.forceInitForTesting(mContext, xmlId);
+        return this;
+    }
+
     public BatteryUsageStatsRule setAveragePower(String key, double value) {
         when(mPowerProfile.getAveragePower(key)).thenReturn(value);
         when(mPowerProfile.getAveragePowerOrDefault(eq(key), anyDouble())).thenReturn(value);
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/MobileRadioPowerCalculatorTest.java b/services/tests/servicestests/src/com/android/server/power/stats/MobileRadioPowerCalculatorTest.java
index 8262fca..5bc73bd 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/MobileRadioPowerCalculatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/MobileRadioPowerCalculatorTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.usage.NetworkStatsManager;
+import android.hardware.radio.V1_5.AccessNetwork;
 import android.net.NetworkCapabilities;
 import android.net.NetworkStats;
 import android.os.BatteryConsumer;
@@ -34,6 +35,8 @@
 import android.os.BatteryUsageStatsQuery;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.telephony.ActivityStatsTechSpecificInfo;
+import android.telephony.CellSignalStrength;
 import android.telephony.DataConnectionRealTimeInfo;
 import android.telephony.ModemActivityInfo;
 import android.telephony.ServiceState;
@@ -43,7 +46,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.os.PowerProfile;
+import com.android.frameworks.servicestests.R;
 
 import com.google.common.collect.Range;
 
@@ -52,28 +55,25 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 
+import java.util.ArrayList;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class MobileRadioPowerCalculatorTest {
     private static final double PRECISION = 0.00001;
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
+    private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 101;
     @Mock
     NetworkStatsManager mNetworkStatsManager;
 
     @Rule
-    public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
-            .setAveragePowerUnspecified(PowerProfile.POWER_RADIO_ACTIVE)
-            .setAveragePowerUnspecified(PowerProfile.POWER_RADIO_ON)
-            .setAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_IDLE, 360.0)
-            .setAveragePower(PowerProfile.POWER_RADIO_SCANNING, 720.0)
-            .setAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX, 1440.0)
-            .setAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_TX,
-                    new double[]{720.0, 1080.0, 1440.0, 1800.0, 2160.0})
-            .initMeasuredEnergyStatsLocked();
+    public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule();
 
     @Test
     public void testCounterBasedModel() {
+        mStatsRule.setTestPowerProfile(R.xml.power_profile_test_modem_calculator)
+                .initMeasuredEnergyStatsLocked();
         BatteryStatsImpl stats = mStatsRule.getBatteryStats();
 
         // Scan for a cell
@@ -85,9 +85,15 @@
         stats.notePhoneStateLocked(ServiceState.STATE_IN_SERVICE, TelephonyManager.SIM_STATE_READY,
                 5000, 5000);
 
+        ArrayList<CellSignalStrength> perRatCellStrength = new ArrayList();
+        CellSignalStrength gsmSignalStrength = mock(CellSignalStrength.class);
+        when(gsmSignalStrength.getLevel()).thenReturn(
+                SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+        perRatCellStrength.add(gsmSignalStrength);
+
         // Note cell signal strength
         SignalStrength signalStrength = mock(SignalStrength.class);
-        when(signalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+        when(signalStrength.getCellSignalStrengths()).thenReturn(perRatCellStrength);
         stats.notePhoneSignalStrengthLocked(signalStrength, 5000, 5000);
 
         stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
@@ -97,10 +103,31 @@
         stats.noteNetworkInterfaceForTransports("cellular",
                 new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
 
+        // Spend some time in each signal strength level. It doesn't matter how long.
+        // The ModemActivityInfo reported level residency should be trusted over the BatteryStats
+        // timers.
+        when(gsmSignalStrength.getLevel()).thenReturn(
+                SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8111, 8111);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_POOR);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8333, 8333);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8666, 8666);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GOOD);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 9110, 9110);
+
+        stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+                9_500_000_000L, APP_UID2, 9500, 9500);
+
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GREAT);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 9665, 9665);
+
         // Note application network activity
         NetworkStats networkStats = new NetworkStats(10000, 1)
                 .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100));
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 150, 2000, 30, 100))
+                .addEntry(new NetworkStats.Entry("cellular", APP_UID2, 0, 0,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 500, 50, 300, 10, 111));
         mStatsRule.setNetworkStats(networkStats);
 
         ModemActivityInfo mai = new ModemActivityInfo(10000, 2000, 3000,
@@ -108,34 +135,331 @@
         stats.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE, 10000, 10000,
                 mNetworkStatsManager);
 
-        mStatsRule.setTime(12_000, 12_000);
+        mStatsRule.setTime(10_000, 10_000);
 
         MobileRadioPowerCalculator calculator =
                 new MobileRadioPowerCalculator(mStatsRule.getPowerProfile());
 
         mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
 
-        UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
-        assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
-                .isWithin(PRECISION).of(0.8);
-        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
-                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
-
         BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
+        //    720 mA * 100 ms  (level 0 TX drain rate * level 0 TX duration)
+        // + 1080 mA * 200 ms  (level 1 TX drain rate * level 1 TX duration)
+        // + 1440 mA * 300 ms  (level 2 TX drain rate * level 2 TX duration)
+        // + 1800 mA * 400 ms  (level 3 TX drain rate * level 3 TX duration)
+        // + 2160 mA * 500 ms  (level 4 TX drain rate * level 4 TX duration)
+        // + 1440 mA * 600 ms  (RX drain rate * RX duration)
+        // +  360 mA * 3000 ms (idle drain rate * idle duration)
+        // +   70 mA * 2000 ms (sleep drain rate * sleep duration)
+        // _________________
+        // =    4604000 mA-ms or 1.27888 mA-h
         assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
-                .isWithin(PRECISION).of(2.2444);
+                .isWithin(PRECISION).of(1.27888);
         assertThat(deviceConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
                 .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
 
+        //    720 mA * 100 ms  (level 0 TX drain rate * level 0 TX duration)
+        // + 1080 mA * 200 ms  (level 1 TX drain rate * level 1 TX duration)
+        // + 1440 mA * 300 ms  (level 2 TX drain rate * level 2 TX duration)
+        // + 1800 mA * 400 ms  (level 3 TX drain rate * level 3 TX duration)
+        // + 2160 mA * 500 ms  (level 4 TX drain rate * level 4 TX duration)
+        // + 1440 mA * 600 ms  (RX drain rate * RX duration)
+        // _________________
+        // =    3384000 mA-ms or 0.94 mA-h
         BatteryConsumer appsConsumer = mStatsRule.getAppsBatteryConsumer();
         assertThat(appsConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
-                .isWithin(PRECISION).of(0.8);
+                .isWithin(PRECISION).of(0.94);
         assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
                 .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        // 3/4 of total packets were sent by APP_UID so 75% of total
+        UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
+        assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(0.705);
+        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        // Rest should go to the other app
+        UidBatteryConsumer uidConsumer2 = mStatsRule.getUidBatteryConsumer(APP_UID2);
+        assertThat(uidConsumer2.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(0.235);
+        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+    }
+
+    @Test
+    public void testCounterBasedModel_multipleDefinedRat() {
+        mStatsRule.setTestPowerProfile(R.xml.power_profile_test_modem_calculator_multiactive)
+                .initMeasuredEnergyStatsLocked();
+        BatteryStatsImpl stats = mStatsRule.getBatteryStats();
+
+        // Scan for a cell
+        stats.notePhoneStateLocked(ServiceState.STATE_OUT_OF_SERVICE,
+                TelephonyManager.SIM_STATE_READY,
+                2000, 2000);
+
+        // Found a cell
+        stats.notePhoneStateLocked(ServiceState.STATE_IN_SERVICE, TelephonyManager.SIM_STATE_READY,
+                5000, 5000);
+
+        ArrayList<CellSignalStrength> perRatCellStrength = new ArrayList();
+        CellSignalStrength gsmSignalStrength = mock(CellSignalStrength.class);
+        when(gsmSignalStrength.getLevel()).thenReturn(
+                SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+        perRatCellStrength.add(gsmSignalStrength);
+
+        // Note cell signal strength
+        SignalStrength signalStrength = mock(SignalStrength.class);
+        when(signalStrength.getCellSignalStrengths()).thenReturn(perRatCellStrength);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 5000, 5000);
+
+        stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+                8_000_000_000L, APP_UID, 8000, 8000);
+
+        // Note established network
+        stats.noteNetworkInterfaceForTransports("cellular",
+                new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
+
+        // Spend some time in each signal strength level. It doesn't matter how long.
+        // The ModemActivityInfo reported level residency should be trusted over the BatteryStats
+        // timers.
+        when(gsmSignalStrength.getLevel()).thenReturn(
+                SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8111, 8111);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_POOR);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8333, 8333);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8666, 8666);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GOOD);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 9110, 9110);
+
+        stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+                9_500_000_000L, APP_UID2, 9500, 9500);
+
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GREAT);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 9665, 9665);
+
+        // Note application network activity
+        NetworkStats networkStats = new NetworkStats(10000, 1)
+                .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 150, 2000, 30, 100))
+                .addEntry(new NetworkStats.Entry("cellular", APP_UID2, 0, 0,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 500, 50, 300, 10, 111));
+        mStatsRule.setNetworkStats(networkStats);
+
+        ActivityStatsTechSpecificInfo cdmaInfo = new ActivityStatsTechSpecificInfo(
+                AccessNetwork.CDMA2000, ServiceState.FREQUENCY_RANGE_UNKNOWN,
+                new int[]{10, 11, 12, 13, 14}, 15);
+        ActivityStatsTechSpecificInfo lteInfo = new ActivityStatsTechSpecificInfo(
+                AccessNetwork.EUTRAN, ServiceState.FREQUENCY_RANGE_UNKNOWN,
+                new int[]{20, 21, 22, 23, 24}, 25);
+        ActivityStatsTechSpecificInfo nrLowFreqInfo = new ActivityStatsTechSpecificInfo(
+                AccessNetwork.NGRAN, ServiceState.FREQUENCY_RANGE_LOW,
+                new int[]{30, 31, 32, 33, 34}, 35);
+        ActivityStatsTechSpecificInfo nrMidFreqInfo = new ActivityStatsTechSpecificInfo(
+                AccessNetwork.NGRAN, ServiceState.FREQUENCY_RANGE_MID,
+                new int[]{40, 41, 42, 43, 44}, 45);
+        ActivityStatsTechSpecificInfo nrHighFreqInfo = new ActivityStatsTechSpecificInfo(
+                AccessNetwork.NGRAN, ServiceState.FREQUENCY_RANGE_HIGH,
+                new int[]{50, 51, 52, 53, 54}, 55);
+        ActivityStatsTechSpecificInfo nrMmwaveFreqInfo = new ActivityStatsTechSpecificInfo(
+                AccessNetwork.NGRAN, ServiceState.FREQUENCY_RANGE_MMWAVE,
+                new int[]{60, 61, 62, 63, 64}, 65);
+
+        ActivityStatsTechSpecificInfo[] ratInfos =
+                new ActivityStatsTechSpecificInfo[]{cdmaInfo, lteInfo, nrLowFreqInfo, nrMidFreqInfo,
+                        nrHighFreqInfo, nrMmwaveFreqInfo};
+
+        ModemActivityInfo mai = new ModemActivityInfo(10000, 2000, 3000, ratInfos);
+        stats.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE, 10000, 10000,
+                mNetworkStatsManager);
+
+        mStatsRule.setTime(10_000, 10_000);
+
+        MobileRadioPowerCalculator calculator =
+                new MobileRadioPowerCalculator(mStatsRule.getPowerProfile());
+
+        mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
+
+        BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
+        // CDMA2000 [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+        //   [720, 1080, 1440, 1800, 2160, 1440] mA . [10, 11, 12, 13, 14, 15] ms = 111600 mA-ms
+        // LTE [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+        //   [800, 1200, 1600, 2000, 2400, 2000] mA . [20, 21, 22, 23, 24, 25] ms = 230000 mA-ms
+        // 5G Low Frequency [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+        // (nrFrequency="LOW" values was not defined so fall back to nrFrequency="DEFAULT")
+        //   [999, 1333, 1888, 2222, 2666, 2222] mA . [30, 31, 32, 33, 34, 35] ms = 373449 mA-ms
+        // 5G Mid Frequency [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+        // (nrFrequency="MID" values was not defined so fall back to nrFrequency="DEFAULT")
+        //   [999, 1333, 1888, 2222, 2666, 2222] mA . [40, 41, 42, 43, 44, 45] ms = 486749 mA-ms
+        // 5G High Frequency [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+        //   [1818, 2727, 3636, 4545, 5454, 2727] mA . [50, 51, 52, 53, 54, 55] ms = 1104435 mA-ms
+        // 5G Mmwave Frequency [Tx0, Tx1, Tx2, Tx3, Tx4, Rx] drain * duration
+        //   [2345, 3456, 4567, 5678, 6789, 3456] mA . [60, 61, 62, 63, 64, 65] ms = 1651520 mA-ms
+        // _________________
+        // =    3957753 mA-ms or 1.09938 mA-h active consumption
+        //
+        // Idle drain rate * idle duration
+        //   360 mA * 3000 ms = 1080000 mA-ms
+        // Sleep drain rate * sleep duration
+        //   70 mA * 2000 ms = 140000 mA-ms
+        // _________________
+        // =    5177753 mA-ms or 1.43826 mA-h total consumption
+        assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(1.43826);
+        assertThat(deviceConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        //    720 mA * 100 ms  (level 0 TX drain rate * level 0 TX duration)
+        // + 1080 mA * 200 ms  (level 1 TX drain rate * level 1 TX duration)
+        // + 1440 mA * 300 ms  (level 2 TX drain rate * level 2 TX duration)
+        // + 1800 mA * 400 ms  (level 3 TX drain rate * level 3 TX duration)
+        // + 2160 mA * 500 ms  (level 4 TX drain rate * level 4 TX duration)
+        // + 1440 mA * 600 ms  (RX drain rate * RX duration)
+        // _________________
+        // =    3384000 mA-ms or 0.94 mA-h
+        BatteryConsumer appsConsumer = mStatsRule.getAppsBatteryConsumer();
+        assertThat(appsConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(1.09938);
+        assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        // 3/4 of total packets were sent by APP_UID so 75% of total
+        UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
+        assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(0.82453);
+        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        // Rest should go to the other app
+        UidBatteryConsumer uidConsumer2 = mStatsRule.getUidBatteryConsumer(APP_UID2);
+        assertThat(uidConsumer2.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(0.27484);
+        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+    }
+
+    @Test
+    public void testCounterBasedModel_legacyPowerProfile() {
+        mStatsRule.setTestPowerProfile(R.xml.power_profile_test_legacy_modem)
+                .initMeasuredEnergyStatsLocked();
+        BatteryStatsImpl stats = mStatsRule.getBatteryStats();
+
+        // Scan for a cell
+        stats.notePhoneStateLocked(ServiceState.STATE_OUT_OF_SERVICE,
+                TelephonyManager.SIM_STATE_READY,
+                2000, 2000);
+
+        // Found a cell
+        stats.notePhoneStateLocked(ServiceState.STATE_IN_SERVICE, TelephonyManager.SIM_STATE_READY,
+                5000, 5000);
+
+        ArrayList<CellSignalStrength> perRatCellStrength = new ArrayList();
+        CellSignalStrength gsmSignalStrength = mock(CellSignalStrength.class);
+        when(gsmSignalStrength.getLevel()).thenReturn(
+                SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+        perRatCellStrength.add(gsmSignalStrength);
+
+        // Note cell signal strength
+        SignalStrength signalStrength = mock(SignalStrength.class);
+        when(signalStrength.getCellSignalStrengths()).thenReturn(perRatCellStrength);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 5000, 5000);
+
+        stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+                8_000_000_000L, APP_UID, 8000, 8000);
+
+        // Note established network
+        stats.noteNetworkInterfaceForTransports("cellular",
+                new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
+
+        // Spend some time in each signal strength level. It doesn't matter how long.
+        // The ModemActivityInfo reported level residency should be trusted over the BatteryStats
+        // timers.
+        when(gsmSignalStrength.getLevel()).thenReturn(
+                SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8111, 8111);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_POOR);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8333, 8333);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8666, 8666);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GOOD);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 9110, 9110);
+
+        stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
+                9_500_000_000L, APP_UID2, 9500, 9500);
+
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GREAT);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 9665, 9665);
+
+        // Note application network activity
+        NetworkStats networkStats = new NetworkStats(10000, 1)
+                .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 150, 2000, 30, 100))
+                .addEntry(new NetworkStats.Entry("cellular", APP_UID2, 0, 0,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 500, 50, 300, 10, 111));
+        mStatsRule.setNetworkStats(networkStats);
+
+        ModemActivityInfo mai = new ModemActivityInfo(10000, 2000, 3000,
+                new int[]{100, 200, 300, 400, 500}, 600);
+        stats.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE, 10000, 10000,
+                mNetworkStatsManager);
+
+        mStatsRule.setTime(10_000, 10_000);
+
+        MobileRadioPowerCalculator calculator =
+                new MobileRadioPowerCalculator(mStatsRule.getPowerProfile());
+
+        mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
+
+        BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
+        //    720 mA * 100 ms  (level 0 TX drain rate * level 0 TX duration)
+        // + 1080 mA * 200 ms  (level 1 TX drain rate * level 1 TX duration)
+        // + 1440 mA * 300 ms  (level 2 TX drain rate * level 2 TX duration)
+        // + 1800 mA * 400 ms  (level 3 TX drain rate * level 3 TX duration)
+        // + 2160 mA * 500 ms  (level 4 TX drain rate * level 4 TX duration)
+        // + 1440 mA * 600 ms  (RX drain rate * RX duration)
+        // +  360 mA * 3000 ms (idle drain rate * idle duration)
+        // +   70 mA * 2000 ms (sleep drain rate * sleep duration)
+        // _________________
+        // =    4604000 mA-ms or 1.27888 mA-h
+        assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(1.27888);
+        assertThat(deviceConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        //    720 mA * 100 ms  (level 0 TX drain rate * level 0 TX duration)
+        // + 1080 mA * 200 ms  (level 1 TX drain rate * level 1 TX duration)
+        // + 1440 mA * 300 ms  (level 2 TX drain rate * level 2 TX duration)
+        // + 1800 mA * 400 ms  (level 3 TX drain rate * level 3 TX duration)
+        // + 2160 mA * 500 ms  (level 4 TX drain rate * level 4 TX duration)
+        // + 1440 mA * 600 ms  (RX drain rate * RX duration)
+        // _________________
+        // =    3384000 mA-ms or 0.94 mA-h
+        BatteryConsumer appsConsumer = mStatsRule.getAppsBatteryConsumer();
+        assertThat(appsConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(0.94);
+        assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        // 3/4 of total packets were sent by APP_UID so 75% of total
+        UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
+        assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(0.705);
+        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+        // Rest should go to the other app
+        UidBatteryConsumer uidConsumer2 = mStatsRule.getUidBatteryConsumer(APP_UID2);
+        assertThat(uidConsumer2.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(0.235);
+        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
     }
 
     @Test
     public void testTimerBasedModel_byProcessState() {
+        mStatsRule.setTestPowerProfile(R.xml.power_profile_test_legacy_modem)
+                .initMeasuredEnergyStatsLocked();
         BatteryStatsImpl stats = mStatsRule.getBatteryStats();
         BatteryStatsImpl.Uid uid = stats.getUidStatsLocked(APP_UID);
 
@@ -148,13 +472,19 @@
                 TelephonyManager.SIM_STATE_READY,
                 2000, 2000);
 
+        ArrayList<CellSignalStrength> perRatCellStrength = new ArrayList();
+        CellSignalStrength gsmSignalStrength = mock(CellSignalStrength.class);
+        when(gsmSignalStrength.getLevel()).thenReturn(
+                SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+        perRatCellStrength.add(gsmSignalStrength);
+
         // Found a cell
         stats.notePhoneStateLocked(ServiceState.STATE_IN_SERVICE, TelephonyManager.SIM_STATE_READY,
                 5000, 5000);
 
         // Note cell signal strength
         SignalStrength signalStrength = mock(SignalStrength.class);
-        when(signalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+        when(signalStrength.getCellSignalStrengths()).thenReturn(perRatCellStrength);
         stats.notePhoneSignalStrengthLocked(signalStrength, 5000, 5000);
 
         stats.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH,
@@ -164,10 +494,25 @@
         stats.noteNetworkInterfaceForTransports("cellular",
                 new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
 
+        // Spend some time in each signal strength level. It doesn't matter how long.
+        // The ModemActivityInfo reported level residency should be trusted over the BatteryStats
+        // timers.
+        when(gsmSignalStrength.getLevel()).thenReturn(
+                SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8111, 8111);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_POOR);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8333, 8333);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_MODERATE);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 8666, 8666);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GOOD);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 9110, 9110);
+        when(gsmSignalStrength.getLevel()).thenReturn(SignalStrength.SIGNAL_STRENGTH_GREAT);
+        stats.notePhoneSignalStrengthLocked(signalStrength, 9665, 9665);
+
         // Note application network activity
         mStatsRule.setNetworkStats(new NetworkStats(10000, 1)
                 .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
-                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100)));
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100)));
 
         stats.noteModemControllerActivity(null, POWER_DATA_UNAVAILABLE, 10000, 10000,
                 mNetworkStatsManager);
@@ -177,7 +522,7 @@
 
         mStatsRule.setNetworkStats(new NetworkStats(12000, 1)
                 .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
-                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 250, 2000, 80, 200)));
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 250, 2000, 80, 200)));
 
         stats.noteModemControllerActivity(null, POWER_DATA_UNAVAILABLE, 12000, 12000,
                 mNetworkStatsManager);
@@ -199,6 +544,8 @@
         MobileRadioPowerCalculator calculator =
                 new MobileRadioPowerCalculator(mStatsRule.getPowerProfile());
 
+        mStatsRule.setTime(12_000, 12_000);
+
         mStatsRule.apply(new BatteryUsageStatsQuery.Builder()
                 .powerProfileModeledOnly()
                 .includePowerModels()
@@ -217,6 +564,10 @@
                 BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
                 BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
 
+
+        assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(1.6);
+
         assertThat(uidConsumer.getConsumedPower(foreground)).isWithin(PRECISION).of(1.2);
         assertThat(uidConsumer.getConsumedPower(background)).isWithin(PRECISION).of(0.4);
         assertThat(uidConsumer.getConsumedPower(fgs)).isWithin(PRECISION).of(0);
@@ -224,6 +575,8 @@
 
     @Test
     public void testMeasuredEnergyBasedModel() {
+        mStatsRule.setTestPowerProfile(R.xml.power_profile_test_legacy_modem)
+                .initMeasuredEnergyStatsLocked();
         BatteryStatsImpl stats = mStatsRule.getBatteryStats();
 
         // Scan for a cell
@@ -264,12 +617,6 @@
 
         mStatsRule.apply(calculator);
 
-        UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
-        assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
-                .isWithin(PRECISION).of(1.53934);
-        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
-                .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
-
         BatteryConsumer deviceConsumer = mStatsRule.getDeviceBatteryConsumer();
         // 10_000_000 micro-Coulomb * 1/1000 milli/micro * 1/3600 hour/second = 2.77778 mAh
         assertThat(deviceConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
@@ -282,10 +629,18 @@
                 .isWithin(PRECISION).of(1.53934);
         assertThat(appsConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
                 .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
+
+        UidBatteryConsumer uidConsumer = mStatsRule.getUidBatteryConsumer(APP_UID);
+        assertThat(uidConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isWithin(PRECISION).of(1.53934);
+        assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO))
+                .isEqualTo(BatteryConsumer.POWER_MODEL_MEASURED_ENERGY);
     }
 
     @Test
     public void testMeasuredEnergyBasedModel_byProcessState() {
+        mStatsRule.setTestPowerProfile(R.xml.power_profile_test_legacy_modem)
+                .initMeasuredEnergyStatsLocked();
         BatteryStatsImpl stats = mStatsRule.getBatteryStats();
         BatteryStatsImpl.Uid uid = stats.getUidStatsLocked(APP_UID);
 
@@ -317,7 +672,7 @@
         // Note application network activity
         mStatsRule.setNetworkStats(new NetworkStats(10000, 1)
                 .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
-                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100)));
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 100, 2000, 20, 100)));
 
         stats.noteModemControllerActivity(null, 10_000_000, 10000, 10000, mNetworkStatsManager);
 
@@ -326,7 +681,7 @@
 
         mStatsRule.setNetworkStats(new NetworkStats(12000, 1)
                 .addEntry(new NetworkStats.Entry("cellular", APP_UID, 0, 0,
-                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 250, 2000, 80, 200)));
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1000, 250, 2000, 80, 200)));
 
         stats.noteModemControllerActivity(null, 15_000_000, 12000, 12000, mNetworkStatsManager);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index a03a1b4..ebcb498 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -234,6 +234,37 @@
     }
 
     @Test
+    public void testScheduleRepostsForLongTagPersistedNotification() throws Exception {
+        String longTag = "A".repeat(66000);
+        NotificationRecord r = getNotificationRecord("pkg", 1, longTag, UserHandle.SYSTEM);
+        mSnoozeHelper.snooze(r, 0);
+
+        // We store the full key in temp storage.
+        ArgumentCaptor<PendingIntent> captor = ArgumentCaptor.forClass(PendingIntent.class);
+        verify(mAm).setExactAndAllowWhileIdle(anyInt(), anyLong(), captor.capture());
+        assertEquals(66010, captor.getValue().getIntent().getStringExtra(EXTRA_KEY).length());
+
+        TypedXmlSerializer serializer = Xml.newFastSerializer();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+        serializer.startDocument(null, true);
+        mSnoozeHelper.writeXml(serializer);
+        serializer.endDocument();
+        serializer.flush();
+
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), "utf-8");
+        mSnoozeHelper.readXml(parser, 4);
+
+        mSnoozeHelper.scheduleRepostsForPersistedNotifications(5);
+
+        // We trim the key in persistent storage.
+        verify(mAm, times(2)).setExactAndAllowWhileIdle(anyInt(), anyLong(), captor.capture());
+        assertEquals(1000, captor.getValue().getIntent().getStringExtra(EXTRA_KEY).length());
+    }
+
+    @Test
     public void testSnoozeForTime() throws Exception {
         NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
         mSnoozeHelper.snooze(r, 1000);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 85c4975..4e001fe 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -106,7 +106,7 @@
         mTransition.mParticipants.add(mTopActivity);
         mTopActivity.mTransitionController.moveToCollecting(mTransition);
         // becomes invisible when covered by mTopActivity
-        mTrampolineActivity.mVisibleRequested = false;
+        mTrampolineActivity.setVisibleRequested(false);
     }
 
     private <T> T verifyAsync(T mock) {
@@ -235,7 +235,7 @@
     public void testOnActivityLaunchCancelled_hasDrawn() {
         onActivityLaunched(mTopActivity);
 
-        mTopActivity.mVisibleRequested = true;
+        mTopActivity.setVisibleRequested(true);
         doReturn(true).when(mTopActivity).isReportedDrawn();
 
         // Cannot time already-visible activities.
@@ -258,7 +258,7 @@
         notifyActivityLaunching(noDrawnActivity.intent);
         notifyAndVerifyActivityLaunched(noDrawnActivity);
 
-        noDrawnActivity.mVisibleRequested = false;
+        noDrawnActivity.setVisibleRequested(false);
         mActivityMetricsLogger.notifyVisibilityChanged(noDrawnActivity);
 
         verifyAsync(mLaunchObserver).onActivityLaunchCancelled(eqLastStartedId(noDrawnActivity));
@@ -286,7 +286,7 @@
 
         clearInvocations(mLaunchObserver);
         mLaunchTopByTrampoline = true;
-        mTopActivity.mVisibleRequested = false;
+        mTopActivity.setVisibleRequested(false);
         notifyActivityLaunching(mTopActivity.intent);
         // It should schedule a message with UNKNOWN_VISIBILITY_CHECK_DELAY_MS to check whether
         // the launch event is still valid.
@@ -314,7 +314,7 @@
         // Create an invisible event that should be cancelled after the next event starts.
         final ActivityRecord prev = new ActivityBuilder(mAtm).setCreateTask(true).build();
         onActivityLaunched(prev);
-        prev.mVisibleRequested = false;
+        prev.setVisibleRequested(false);
 
         mActivityOptions = ActivityOptions.makeBasic();
         mActivityOptions.setSourceInfo(SourceInfo.TYPE_LAUNCHER, SystemClock.uptimeMillis() - 10);
@@ -561,7 +561,7 @@
     @Test
     public void testConsecutiveLaunchWithDifferentWindowingMode() {
         mTopActivity.setWindowingMode(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
-        mTrampolineActivity.mVisibleRequested = true;
+        mTrampolineActivity.setVisibleRequested(true);
         onActivityLaunched(mTrampolineActivity);
         mActivityMetricsLogger.notifyActivityLaunching(mTopActivity.intent,
                 mTrampolineActivity /* caller */, mTrampolineActivity.getUid());
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index a410eed..f983fa9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -917,7 +917,7 @@
         // Prepare the activity record to be ready for immediate removal. It should be invisible and
         // have no process. Otherwise, request to finish it will send a message to client first.
         activity.setState(STOPPED, "test");
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.nowVisible = false;
         // Set process to 'null' to allow immediate removal, but don't call mActivity.setProcess() -
         // this will cause NPE when updating task's process.
@@ -927,7 +927,7 @@
         // next activity reports idle to destroy it.
         final ActivityRecord topActivity = new ActivityBuilder(mAtm)
                 .setTask(activity.getTask()).build();
-        topActivity.mVisibleRequested = true;
+        topActivity.setVisibleRequested(true);
         topActivity.nowVisible = true;
         topActivity.setState(RESUMED, "test");
 
@@ -1082,7 +1082,7 @@
         final ActivityRecord activity = createActivityWithTask();
         clearInvocations(activity.mDisplayContent);
         activity.finishing = false;
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         activity.setState(RESUMED, "test");
         activity.finishIfPossible("test", false /* oomAdj */);
 
@@ -1099,7 +1099,7 @@
         final ActivityRecord activity = createActivityWithTask();
         clearInvocations(activity.mDisplayContent);
         activity.finishing = false;
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         activity.setState(PAUSED, "test");
         activity.finishIfPossible("test", false /* oomAdj */);
 
@@ -1118,7 +1118,7 @@
         // Put an activity on top of test activity to make it invisible and prevent us from
         // accidentally resuming the topmost one again.
         new ActivityBuilder(mAtm).build();
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.setState(STOPPED, "test");
 
         activity.finishIfPossible("test", false /* oomAdj */);
@@ -1136,7 +1136,7 @@
         final TestTransitionPlayer testPlayer = registerTestTransitionPlayer();
         final ActivityRecord activity = createActivityWithTask();
         activity.finishing = false;
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         activity.setState(RESUMED, "test");
         activity.finishIfPossible("test", false /* oomAdj */);
 
@@ -1273,7 +1273,7 @@
         final ActivityRecord currentTop = createActivityWithTask();
         final Task task = currentTop.getTask();
 
-        currentTop.mVisibleRequested = currentTop.nowVisible = true;
+        currentTop.setVisibleRequested(currentTop.nowVisible = true);
 
         // Simulates that {@code currentTop} starts an existing activity from background (so its
         // state is stopped) and the starting flow just goes to place it at top.
@@ -1300,7 +1300,7 @@
         final ActivityRecord bottomActivity = createActivityWithTask();
         final ActivityRecord topActivity = new ActivityBuilder(mAtm)
                 .setTask(bottomActivity.getTask()).build();
-        topActivity.mVisibleRequested = true;
+        topActivity.setVisibleRequested(true);
         // simulating bottomActivity as a trampoline activity.
         bottomActivity.setState(RESUMED, "test");
         bottomActivity.finishIfPossible("test", false);
@@ -1316,13 +1316,13 @@
         final ActivityRecord activity = createActivityWithTask();
         final ActivityRecord topActivity = new ActivityBuilder(mAtm)
                 .setTask(activity.getTask()).build();
-        topActivity.mVisibleRequested = true;
+        topActivity.setVisibleRequested(true);
         topActivity.nowVisible = true;
         topActivity.finishing = true;
         topActivity.setState(PAUSED, "true");
         // Mark the bottom activity as not visible, so that we will wait for it before removing
         // the top one.
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.nowVisible = false;
         activity.setState(STOPPED, "test");
 
@@ -1346,13 +1346,13 @@
         final ActivityRecord topActivity = createActivityWithTask();
         mDisplayContent.setIsSleeping(true);
         doReturn(true).when(activity).shouldBeVisible();
-        topActivity.mVisibleRequested = false;
+        topActivity.setVisibleRequested(false);
         topActivity.nowVisible = false;
         topActivity.finishing = true;
         topActivity.setState(STOPPED, "true");
 
         // Mark the activity behind (on a separate task) as not visible
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.nowVisible = false;
         activity.setState(STOPPED, "test");
 
@@ -1370,13 +1370,13 @@
         final ActivityRecord activity = createActivityWithTask();
         final ActivityRecord topActivity = new ActivityBuilder(mAtm)
                 .setTask(activity.getTask()).build();
-        topActivity.mVisibleRequested = false;
+        topActivity.setVisibleRequested(false);
         topActivity.nowVisible = false;
         topActivity.finishing = true;
         topActivity.setState(STOPPED, "true");
         // Mark the bottom activity as not visible, so that we would wait for it before removing
         // the top one.
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.nowVisible = false;
         activity.setState(STOPPED, "test");
 
@@ -1394,12 +1394,12 @@
         final ActivityRecord activity = createActivityWithTask();
         final ActivityRecord topActivity = new ActivityBuilder(mAtm)
                 .setTask(activity.getTask()).build();
-        topActivity.mVisibleRequested = true;
+        topActivity.setVisibleRequested(true);
         topActivity.nowVisible = true;
         topActivity.finishing = true;
         topActivity.setState(PAUSED, "true");
         // Mark the bottom activity as already visible, so that there is no need to wait for it.
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         activity.nowVisible = true;
         activity.setState(RESUMED, "test");
 
@@ -1417,12 +1417,12 @@
         final ActivityRecord activity = createActivityWithTask();
         final ActivityRecord topActivity = new ActivityBuilder(mAtm)
                 .setTask(activity.getTask()).build();
-        topActivity.mVisibleRequested = false;
+        topActivity.setVisibleRequested(false);
         topActivity.nowVisible = false;
         topActivity.finishing = true;
         topActivity.setState(STOPPED, "true");
         // Mark the bottom activity as already visible, so that there is no need to wait for it.
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         activity.nowVisible = true;
         activity.setState(RESUMED, "test");
 
@@ -1440,12 +1440,12 @@
         final ActivityRecord activity = createActivityWithTask();
         final ActivityRecord topActivity = new ActivityBuilder(mAtm)
                 .setTask(activity.getTask()).build();
-        topActivity.mVisibleRequested = true;
+        topActivity.setVisibleRequested(true);
         topActivity.nowVisible = true;
         topActivity.finishing = true;
         topActivity.setState(PAUSED, "true");
         // Mark the bottom activity as already visible, so that there is no need to wait for it.
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         activity.nowVisible = true;
         activity.setState(RESUMED, "test");
 
@@ -1454,7 +1454,7 @@
         final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
         final ActivityRecord focusedActivity = stack.getTopMostActivity();
         focusedActivity.nowVisible = true;
-        focusedActivity.mVisibleRequested = true;
+        focusedActivity.setVisibleRequested(true);
         focusedActivity.setState(RESUMED, "test");
         stack.setResumedActivity(focusedActivity, "test");
 
@@ -1476,7 +1476,7 @@
         int displayId = activity.getDisplayId();
         doReturn(true).when(keyguardController).isKeyguardLocked(eq(displayId));
         final ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build();
-        topActivity.mVisibleRequested = true;
+        topActivity.setVisibleRequested(true);
         topActivity.nowVisible = true;
         topActivity.setState(RESUMED, "true");
         doCallRealMethod().when(mRootWindowContainer).ensureActivitiesVisible(
@@ -1515,12 +1515,12 @@
         final ActivityRecord activity = createActivityWithTask();
         final Task task = activity.getTask();
         final ActivityRecord firstActivity = new ActivityBuilder(mAtm).setTask(task).build();
-        firstActivity.mVisibleRequested = false;
+        firstActivity.setVisibleRequested(false);
         firstActivity.nowVisible = false;
         firstActivity.setState(STOPPED, "test");
 
         final ActivityRecord secondActivity = new ActivityBuilder(mAtm).setTask(task).build();
-        secondActivity.mVisibleRequested = true;
+        secondActivity.setVisibleRequested(true);
         secondActivity.nowVisible = true;
         secondActivity.setState(secondActivityState, "test");
 
@@ -1530,7 +1530,7 @@
         } else {
             translucentActivity = new ActivityBuilder(mAtm).setTask(task).build();
         }
-        translucentActivity.mVisibleRequested = true;
+        translucentActivity.setVisibleRequested(true);
         translucentActivity.nowVisible = true;
         translucentActivity.setState(RESUMED, "test");
 
@@ -1546,7 +1546,7 @@
 
         // Finish the first activity
         firstActivity.finishing = true;
-        firstActivity.mVisibleRequested = true;
+        firstActivity.setVisibleRequested(true);
         firstActivity.completeFinishing("test");
         verify(firstActivity.mDisplayContent, times(2)).ensureActivitiesVisible(null /* starting */,
                 0 /* configChanges */ , false /* preserveWindows */,
@@ -1614,7 +1614,7 @@
         }, true /* traverseTopToBottom */);
         activity.setState(STARTED, "test");
         activity.finishing = true;
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
 
         // Try to finish the last activity above the home stack.
         activity.completeFinishing("test");
@@ -1909,7 +1909,7 @@
         // Simulate that the activity requests the same orientation as display.
         activity.setOrientation(display.getConfiguration().orientation);
         // Skip the real freezing.
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         clearInvocations(activity);
         activity.onCancelFixedRotationTransform(originalRotation);
         // The implementation of cancellation must be executed.
@@ -2535,7 +2535,7 @@
 
         activity.setOccludesParent(true);
         activity.setVisible(false);
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         // Can not specify orientation if app isn't visible even though it occludes parent.
         assertEquals(SCREEN_ORIENTATION_UNSET, activity.getOrientation());
         // Can specify orientation if the current orientation candidate is orientation behind.
@@ -2910,7 +2910,7 @@
         task.addChild(taskFragment2, POSITION_TOP);
         final ActivityRecord activity2 = new ActivityBuilder(mAtm)
                 .setResizeMode(ActivityInfo.RESIZE_MODE_UNRESIZEABLE).build();
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
         taskFragment2.addChild(activity2);
         assertTrue(activity2.isResizeable());
         activity1.reparent(taskFragment1, POSITION_TOP);
@@ -3055,7 +3055,7 @@
                 .setCreateTask(true).build();
         // By default, activity is visible.
         assertTrue(activity.isVisible());
-        assertTrue(activity.mVisibleRequested);
+        assertTrue(activity.isVisibleRequested());
         assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
 
@@ -3064,7 +3064,7 @@
         // until we verify no logic relies on this behavior, we'll keep this as is.
         activity.setVisibility(true);
         assertTrue(activity.isVisible());
-        assertTrue(activity.mVisibleRequested);
+        assertTrue(activity.isVisibleRequested());
         assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
     }
@@ -3075,7 +3075,7 @@
                 .setCreateTask(true).build();
         // By default, activity is visible.
         assertTrue(activity.isVisible());
-        assertTrue(activity.mVisibleRequested);
+        assertTrue(activity.isVisibleRequested());
         assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
 
@@ -3083,7 +3083,7 @@
         // animation should be applied on this activity.
         activity.setVisibility(false);
         assertTrue(activity.isVisible());
-        assertFalse(activity.mVisibleRequested);
+        assertFalse(activity.isVisibleRequested());
         assertFalse(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertTrue(activity.mDisplayContent.mClosingApps.contains(activity));
     }
@@ -3095,7 +3095,7 @@
         // Activiby is invisible. However ATMS requests it to become visible, since this is a top
         // activity.
         assertFalse(activity.isVisible());
-        assertTrue(activity.mVisibleRequested);
+        assertTrue(activity.isVisibleRequested());
         assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
 
@@ -3103,7 +3103,7 @@
         // animation should be applied on this activity.
         activity.setVisibility(true);
         assertFalse(activity.isVisible());
-        assertTrue(activity.mVisibleRequested);
+        assertTrue(activity.isVisibleRequested());
         assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
 
@@ -3126,7 +3126,7 @@
         // Activiby is invisible. However ATMS requests it to become visible, since this is a top
         // activity.
         assertFalse(activity.isVisible());
-        assertTrue(activity.mVisibleRequested);
+        assertTrue(activity.isVisibleRequested());
         assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
 
@@ -3134,7 +3134,7 @@
         // transition should be applied on this activity.
         activity.setVisibility(false);
         assertFalse(activity.isVisible());
-        assertFalse(activity.mVisibleRequested);
+        assertFalse(activity.isVisibleRequested());
         assertFalse(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
     }
@@ -3549,12 +3549,12 @@
         activity.reparent(taskFragment, POSITION_TOP);
 
         // Ensure the activity visibility is updated even it is not shown to current user.
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         doReturn(false).when(activity).showToCurrentUser();
         spyOn(taskFragment);
         doReturn(false).when(taskFragment).shouldBeVisible(any());
         display.ensureActivitiesVisible(null, 0, false, false);
-        assertFalse(activity.mVisibleRequested);
+        assertFalse(activity.isVisibleRequested());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 0743ef4..ee31748 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -514,7 +514,9 @@
                 .setCreateActivity(true)
                 .build()
                 .getTopMostActivity();
-        splitPrimaryActivity.mVisibleRequested = splitSecondActivity.mVisibleRequested = true;
+
+        splitPrimaryActivity.setVisibleRequested(true);
+        splitSecondActivity.setVisibleRequested(true);
 
         assertEquals(splitOrg.mPrimary, splitPrimaryActivity.getRootTask());
         assertEquals(splitOrg.mSecondary, splitSecondActivity.getRootTask());
@@ -527,7 +529,7 @@
                 .setCreateActivity(true).build().getTopMostActivity();
         final ActivityRecord translucentActivity = new TaskBuilder(mSupervisor)
                 .setCreateActivity(true).build().getTopMostActivity();
-        assertTrue(activity.mVisibleRequested);
+        assertTrue(activity.isVisibleRequested());
 
         final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
                 false /* mockGetRootTask */);
@@ -1050,7 +1052,7 @@
                         ACTIVITY_TYPE_STANDARD, false /* onTop */));
         // Activity should start invisible since we are bringing it to front.
         singleTaskActivity.setVisible(false);
-        singleTaskActivity.mVisibleRequested = false;
+        singleTaskActivity.setVisibleRequested(false);
 
         // Create another activity on top of the secondary display.
         final Task topStack = secondaryTaskContainer.createRootTask(WINDOWING_MODE_FULLSCREEN,
@@ -1268,7 +1270,7 @@
         final ActivityStarter starter = prepareStarter(0 /* flags */);
         final ActivityRecord target = new ActivityBuilder(mAtm).setCreateTask(true).build();
         starter.mStartActivity = target;
-        target.mVisibleRequested = false;
+        target.setVisibleRequested(false);
         target.setTurnScreenOn(true);
         // Assume the flag was consumed by relayout.
         target.setCurrentLaunchCanTurnScreenOn(false);
@@ -1589,10 +1591,10 @@
         final ActivityRecord activityTop = new ActivityBuilder(mAtm).setTask(task).build();
 
         activityBot.setVisible(false);
-        activityBot.mVisibleRequested = false;
+        activityBot.setVisibleRequested(false);
 
         assertTrue(activityTop.isVisible());
-        assertTrue(activityTop.mVisibleRequested);
+        assertTrue(activityTop.isVisibleRequested());
 
         final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_REORDER_TO_FRONT
                         | FLAG_ACTIVITY_NEW_TASK, false /* mockGetRootTask */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index 2fccd64..368b750 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -344,7 +344,7 @@
 
         // Assume the activity is finishing and hidden because it was crashed.
         activity.finishing = true;
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.setVisible(false);
         activity.getTask().setPausingActivity(activity);
         homeActivity.setState(PAUSED, "test");
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index fb94147c..c73237e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -122,7 +122,7 @@
         final ActivityRecord top = createActivityRecord(task);
         top.setState(ActivityRecord.State.RESUMED, "test");
         behind.setState(ActivityRecord.State.STARTED, "test");
-        behind.mVisibleRequested = true;
+        behind.setVisibleRequested(true);
 
         task.removeActivities("test", false /* excludingTaskOverlay */);
         assertFalse(mDisplayContent.mAppTransition.isReady());
@@ -294,7 +294,7 @@
 
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
         activity2.setVisible(false);
-        activity2.mVisibleRequested = false;
+        activity2.setVisibleRequested(false);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
         opening.add(activity1);
@@ -319,12 +319,12 @@
         //                   +- [Task2] - [ActivityRecord2] (opening, visible)
         final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
         activity1.setVisible(true);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         activity1.mRequestForceTransition = true;
 
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
         activity2.setVisible(false);
-        activity2.mVisibleRequested = false;
+        activity2.setVisibleRequested(false);
         activity2.mRequestForceTransition = true;
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
@@ -391,7 +391,7 @@
 
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
         activity2.setVisible(false);
-        activity2.mVisibleRequested = false;
+        activity2.setVisibleRequested(false);
         attrs.setTitle("AppWindow2");
         final TestWindowState appWindow2 = createWindowState(attrs, activity2);
         appWindow2.mWillReplaceWindow = true;
@@ -424,17 +424,17 @@
         //                               +- [ActivityRecord4] (invisible)
         final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
                 activity1.getTask());
         activity2.setVisible(false);
-        activity2.mVisibleRequested = false;
+        activity2.setVisibleRequested(false);
 
         final ActivityRecord activity3 = createActivityRecord(mDisplayContent);
         final ActivityRecord activity4 = createActivityRecord(mDisplayContent,
                 activity3.getTask());
         activity4.setVisible(false);
-        activity4.mVisibleRequested = false;
+        activity4.setVisibleRequested(false);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
         opening.add(activity1);
@@ -459,7 +459,7 @@
         //                            +- [ActivityRecord2] (closing, visible)
         final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
                 activity1.getTask());
 
@@ -490,7 +490,7 @@
 
         final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         activity1.setOccludesParent(false);
 
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
@@ -528,13 +528,13 @@
 
         final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         activity1.setOccludesParent(false);
 
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent,
                 activity1.getTask());
         activity2.setVisible(false);
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
 
         final ActivityRecord activity3 = createActivityRecord(mDisplayContent);
         activity3.setOccludesParent(false);
@@ -567,7 +567,7 @@
         final Task parentTask = createTask(mDisplayContent);
         final ActivityRecord activity1 = createActivityRecordWithParentTask(parentTask);
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         final ActivityRecord activity2 = createActivityRecordWithParentTask(parentTask);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
@@ -600,10 +600,10 @@
         splitRoot1.setAdjacentTaskFragment(splitRoot2);
         final ActivityRecord activity1 = createActivityRecordWithParentTask(splitRoot1);
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         final ActivityRecord activity2 = createActivityRecordWithParentTask(splitRoot2);
         activity2.setVisible(false);
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
         opening.add(activity1);
@@ -625,12 +625,12 @@
         final TaskFragment taskFragment1 = createTaskFragmentWithActivity(parentTask);
         final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
 
         final TaskFragment taskFragment2 = createTaskFragmentWithActivity(parentTask);
         final ActivityRecord activity2 = taskFragment2.getTopMostActivity();
         activity2.setVisible(true);
-        activity2.mVisibleRequested = false;
+        activity2.setVisibleRequested(false);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
         opening.add(activity1);
@@ -654,11 +654,11 @@
         final TaskFragment taskFragment1 = createTaskFragmentWithActivity(task1);
         final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
 
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
         activity2.setVisible(true);
-        activity2.mVisibleRequested = false;
+        activity2.setVisibleRequested(false);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
         opening.add(activity1);
@@ -683,11 +683,11 @@
         final TaskFragment taskFragment1 = createTaskFragmentWithActivity(task1);
         final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
         activity1.setVisible(true);
-        activity1.mVisibleRequested = false;
+        activity1.setVisibleRequested(false);
 
         final ActivityRecord activity2 = createActivityRecord(mDisplayContent);
         activity2.setVisible(false);
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
         opening.add(activity2);
@@ -710,13 +710,13 @@
         //                   +- [Task2] (embedded) - [ActivityRecord2] (opening, invisible)
         final ActivityRecord activity1 = createActivityRecord(mDisplayContent);
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
 
         final Task task2 = createTask(mDisplayContent);
         task2.mRemoveWithTaskOrganizer = true;
         final ActivityRecord activity2 = createActivityRecord(task2);
         activity2.setVisible(false);
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
         opening.add(activity1);
@@ -744,7 +744,7 @@
 
         final ActivityRecord activity1 = createActivityRecord(task);
         activity1.setVisible(false);
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         final ActivityRecord activity2 = createActivityRecord(task);
 
         final ArraySet<ActivityRecord> opening = new ArraySet<>();
@@ -1260,6 +1260,8 @@
     @Test
     public void testTransitionGoodToGoForTaskFragments_detachedApp() {
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer);
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(iOrganizer);
         final Task task = createTask(mDisplayContent);
         final TaskFragment changeTaskFragment =
                 createTaskFragmentWithEmbeddedActivity(task, organizer);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 437eeb1..6b814e6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -385,11 +385,11 @@
         // Simulate activity1 launches activity2.
         final ActivityRecord activity1 = createActivityRecord(task);
         activity1.setVisible(true);
-        activity1.mVisibleRequested = false;
+        activity1.setVisibleRequested(false);
         activity1.allDrawn = true;
         final ActivityRecord activity2 = createActivityRecord(task);
         activity2.setVisible(false);
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
         activity2.allDrawn = true;
 
         dc.mClosingApps.add(activity1);
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index dc3515d..3bce860 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -42,7 +42,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.view.WindowManager;
 import android.window.BackAnimationAdapter;
-import android.window.BackEvent;
+import android.window.BackMotionEvent;
 import android.window.BackNavigationInfo;
 import android.window.IOnBackInvokedCallback;
 import android.window.OnBackInvokedCallback;
@@ -150,6 +150,7 @@
         WindowState window = createWindow(null, WindowManager.LayoutParams.TYPE_WALLPAPER,
                 "Wallpaper");
         addToWindowMap(window, true);
+        makeWindowVisibleAndDrawn(window);
 
         IOnBackInvokedCallback callback = createOnBackInvokedCallback();
         window.setOnBackInvokedCallbackInfo(
@@ -236,6 +237,20 @@
                 1, appLatch.getCount());
     }
 
+    @Test
+    public void backInfoWindowWithoutDrawn() {
+        WindowState window = createWindow(null, WindowManager.LayoutParams.TYPE_APPLICATION,
+                "TestWindow");
+        addToWindowMap(window, true);
+
+        IOnBackInvokedCallback callback = createOnBackInvokedCallback();
+        window.setOnBackInvokedCallbackInfo(
+                new OnBackInvokedCallbackInfo(callback, OnBackInvokedDispatcher.PRIORITY_DEFAULT));
+
+        BackNavigationInfo backNavigationInfo = startBackNavigation();
+        assertThat(backNavigationInfo).isNull();
+    }
+
     private IOnBackInvokedCallback withSystemCallback(Task task) {
         IOnBackInvokedCallback callback = createOnBackInvokedCallback();
         task.getTopMostActivity().getTopChild().setOnBackInvokedCallbackInfo(
@@ -259,11 +274,11 @@
     private IOnBackInvokedCallback createOnBackInvokedCallback() {
         return new IOnBackInvokedCallback.Stub() {
             @Override
-            public void onBackStarted(BackEvent backEvent) {
+            public void onBackStarted(BackMotionEvent backMotionEvent) {
             }
 
             @Override
-            public void onBackProgressed(BackEvent backEvent) {
+            public void onBackProgressed(BackMotionEvent backMotionEvent) {
             }
 
             @Override
@@ -309,6 +324,7 @@
         Mockito.doNothing().when(task).reparentSurfaceControl(any(), any());
         mAtm.setFocusedTask(task.mTaskId, record);
         addToWindowMap(window, true);
+        makeWindowVisibleAndDrawn(window);
         return task;
     }
 
@@ -333,6 +349,8 @@
         addToWindowMap(window1, true);
         addToWindowMap(window2, true);
 
+        makeWindowVisibleAndDrawn(window2);
+
         CrossActivityTestCase testCase = new CrossActivityTestCase();
         testCase.task = task;
         testCase.recordBack = record1;
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index d151188..bc23fa3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -590,7 +590,7 @@
         assertEquals(window1, mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus);
 
         // Make sure top focused display not changed if there is a focused app.
-        window1.mActivityRecord.mVisibleRequested = false;
+        window1.mActivityRecord.setVisibleRequested(false);
         window1.getDisplayContent().setFocusedApp(window1.mActivityRecord);
         updateFocusedWindow();
         assertTrue(!window1.isFocused());
@@ -1106,7 +1106,7 @@
     public void testOrientationBehind() {
         final ActivityRecord prev = new ActivityBuilder(mAtm).setCreateTask(true)
                 .setScreenOrientation(getRotatedOrientation(mDisplayContent)).build();
-        prev.mVisibleRequested = false;
+        prev.setVisibleRequested(false);
         final ActivityRecord top = new ActivityBuilder(mAtm).setCreateTask(true)
                 .setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build();
         assertNotEquals(WindowConfiguration.ROTATION_UNDEFINED,
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
index db1d15a..ba68a25 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -243,7 +243,7 @@
         }
 
         @Override
-        public boolean canShowTasksInRecents() {
+        public boolean canShowTasksInHostDeviceRecents() {
             return true;
         }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index ac2df62..6f2e3f2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -819,7 +819,7 @@
     @Test
     public void testVisibleTask_displayCanNotShowTaskFromRecents_expectNotVisible() {
         final DisplayContent displayContent = addNewDisplayContentAt(DisplayContent.POSITION_TOP);
-        doReturn(false).when(displayContent).canShowTasksInRecents();
+        doReturn(false).when(displayContent).canShowTasksInHostDeviceRecents();
         final Task task = displayContent.getDefaultTaskDisplayArea().createRootTask(
                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */);
         mRecentTasks.add(task);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
index 88e58ea..08635ab 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -172,12 +172,12 @@
         // executed.
         final ActivityRecord activity1 = createActivityRecord(task);
         activity1.setVisible(true);
-        activity1.mVisibleRequested = false;
+        activity1.setVisibleRequested(false);
         activity1.addWindow(createWindowState(new LayoutParams(TYPE_BASE_APPLICATION), activity1));
 
         final ActivityRecord activity2 = createActivityRecord(task);
         activity2.setVisible(false);
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
 
         mDefaultDisplay.getConfiguration().windowConfiguration.setRotation(
                 mDefaultDisplay.getRotation());
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
index a2b4cb8..de3a526 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
@@ -111,7 +111,7 @@
         RecentsAnimationCallbacks recentsAnimation = startRecentsActivity(
                 mRecentsComponent, true /* getRecentsAnimation */);
         // The launch-behind state should make the recents activity visible.
-        assertTrue(recentActivity.mVisibleRequested);
+        assertTrue(recentActivity.isVisibleRequested());
         assertEquals(ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS,
                 mAtm.mDemoteTopAppReasons);
         assertFalse(mAtm.mInternal.useTopSchedGroupForTopProcess());
@@ -119,7 +119,7 @@
         // Simulate the animation is cancelled without changing the stack order.
         recentsAnimation.onAnimationFinished(REORDER_KEEP_IN_PLACE, false /* sendUserLeaveHint */);
         // The non-top recents activity should be invisible by the restored launch-behind state.
-        assertFalse(recentActivity.mVisibleRequested);
+        assertFalse(recentActivity.isVisibleRequested());
         assertEquals(0, mAtm.mDemoteTopAppReasons);
     }
 
@@ -164,7 +164,7 @@
         // The activity is started in background so it should be invisible and will be stopped.
         assertThat(recentsActivity).isNotNull();
         assertThat(mSupervisor.mStoppingActivities).contains(recentsActivity);
-        assertFalse(recentsActivity.mVisibleRequested);
+        assertFalse(recentsActivity.isVisibleRequested());
 
         // Assume it is stopped to test next use case.
         recentsActivity.activityStopped(null /* newIcicle */, null /* newPersistentState */,
@@ -360,7 +360,7 @@
                 true);
 
         // Ensure we find the task for the right user and it is made visible
-        assertTrue(otherUserHomeActivity.mVisibleRequested);
+        assertTrue(otherUserHomeActivity.isVisibleRequested());
     }
 
     private void startRecentsActivity() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
index 881cc5f..fb29d3a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
@@ -1068,7 +1068,7 @@
         activity.app = null;
         overlayActivity.app = null;
         // Simulate the process is dead
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.setState(DESTROYED, "Test");
 
         assertEquals(2, task.getChildCount());
@@ -1205,7 +1205,7 @@
 
         // There is still an activity1 in rootTask1 so the activity2 should be added to finishing
         // list that will be destroyed until idle.
-        rootTask2.getTopNonFinishingActivity().mVisibleRequested = true;
+        rootTask2.getTopNonFinishingActivity().setVisibleRequested(true);
         final ActivityRecord activity2 = finishTopActivity(rootTask2);
         assertEquals(STOPPING, activity2.getState());
         assertThat(mSupervisor.mStoppingActivities).contains(activity2);
@@ -1410,7 +1410,7 @@
         new ActivityBuilder(mAtm).setTask(task).build();
         // The scenario we are testing is when the app isn't visible yet.
         nonTopVisibleActivity.setVisible(false);
-        nonTopVisibleActivity.mVisibleRequested = false;
+        nonTopVisibleActivity.setVisibleRequested(false);
         doReturn(false).when(nonTopVisibleActivity).attachedToProcess();
         doReturn(true).when(nonTopVisibleActivity).shouldBeVisibleUnchecked();
         doNothing().when(mSupervisor).startSpecificActivity(any(), anyBoolean(),
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index f84865b..a17e124 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -174,7 +174,7 @@
         final Task rootTask = new TaskBuilder(mSupervisor).build();
         final Task task1 = new TaskBuilder(mSupervisor).setParentTask(rootTask).build();
         final ActivityRecord activity1 = new ActivityBuilder(mAtm).setTask(task1).build();
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         mWm.mRoot.rankTaskLayers();
 
         assertEquals(1, task1.mLayerRank);
@@ -183,7 +183,7 @@
 
         final Task task2 = new TaskBuilder(mSupervisor).build();
         final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task2).build();
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
         mWm.mRoot.rankTaskLayers();
 
         // Note that ensureActivitiesVisible is disabled in SystemServicesTestRule, so both the
@@ -200,8 +200,8 @@
         assertEquals(2, task2.mLayerRank);
 
         // The rank should be updated to invisible when device went to sleep.
-        activity1.mVisibleRequested = false;
-        activity2.mVisibleRequested = false;
+        activity1.setVisibleRequested(false);
+        activity2.setVisibleRequested(false);
         doReturn(true).when(mAtm).isSleepingOrShuttingDownLocked();
         doReturn(true).when(mRootWindowContainer).putTasksToSleep(anyBoolean(), anyBoolean());
         mSupervisor.mGoingToSleepWakeLock = mock(PowerManager.WakeLock.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index e65610f..13ea99a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -169,7 +169,7 @@
     public void testRestartProcessIfVisible() {
         setUpDisplaySizeWithApp(1000, 2500);
         doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
-        mActivity.mVisibleRequested = true;
+        mActivity.setVisibleRequested(true);
         mActivity.setSavedState(null /* savedState */);
         mActivity.setState(RESUMED, "testRestart");
         prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_UNSPECIFIED);
@@ -553,7 +553,7 @@
         resizeDisplay(display, 900, 1800);
 
         mActivity.setState(STOPPED, "testSizeCompatMode");
-        mActivity.mVisibleRequested = false;
+        mActivity.setVisibleRequested(false);
         mActivity.visibleIgnoringKeyguard = false;
         mActivity.app.setReportedProcState(ActivityManager.PROCESS_STATE_CACHED_ACTIVITY);
         mActivity.app.computeProcessActivityState();
@@ -605,7 +605,7 @@
         // Make the activity resizable again by restarting it
         clearInvocations(mTask);
         mActivity.info.resizeMode = RESIZE_MODE_RESIZEABLE;
-        mActivity.mVisibleRequested = true;
+        mActivity.setVisibleRequested(true);
         mActivity.restartProcessIfVisible();
         // The full lifecycle isn't hooked up so manually set state to resumed
         mActivity.setState(RESUMED, "testHandleActivitySizeCompatModeChanged");
@@ -3188,7 +3188,7 @@
             task.mResizeMode = activity.info.resizeMode;
             task.getRootActivity().info.resizeMode = activity.info.resizeMode;
         }
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         if (maxAspect >= 0) {
             activity.info.setMaxAspectRatio(maxAspect);
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index d5fb1a8..91f8d8d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -398,7 +398,7 @@
                     .setParentTask(rootHomeTask).setCreateTask(true).build();
         }
         homeActivity.setVisible(false);
-        homeActivity.mVisibleRequested = true;
+        homeActivity.setVisibleRequested(true);
         assertFalse(rootHomeTask.isVisible());
 
         assertEquals(defaultTaskDisplayArea.getOrientation(), rootHomeTask.getOrientation());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 2b49314..834302e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -370,7 +370,8 @@
         mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
-        assertTaskFragmentParentInfoChangedTransaction(task);
+        // There will not be TaskFragmentParentInfoChanged because Task visible request is changed
+        // before the organized TaskFragment is added to the Task.
         assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token);
     }
 
@@ -1159,6 +1160,7 @@
         doReturn(false).when(task).shouldBeVisible(any());
 
         // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        clearInvocations(mOrganizer);
         mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 59a31b1..aaf07b9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -135,8 +135,8 @@
         changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
         fillChangeMap(changes, newTask);
         // End states.
-        closing.mVisibleRequested = false;
-        opening.mVisibleRequested = true;
+        closing.setVisibleRequested(false);
+        opening.setVisibleRequested(true);
 
         final int transit = transition.mType;
         int flags = 0;
@@ -199,9 +199,9 @@
         changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
         fillChangeMap(changes, newTask);
         // End states.
-        closing.mVisibleRequested = false;
-        opening.mVisibleRequested = true;
-        opening2.mVisibleRequested = true;
+        closing.setVisibleRequested(false);
+        opening.setVisibleRequested(true);
+        opening2.setVisibleRequested(true);
 
         final int transit = transition.mType;
         int flags = 0;
@@ -250,8 +250,8 @@
         fillChangeMap(changes, tda);
 
         // End states.
-        showing.mVisibleRequested = true;
-        showing2.mVisibleRequested = true;
+        showing.setVisibleRequested(true);
+        showing2.setVisibleRequested(true);
 
         final int transit = transition.mType;
         int flags = 0;
@@ -286,16 +286,16 @@
 
         final Task openTask = createTask(mDisplayContent);
         final ActivityRecord opening = createActivityRecord(openTask);
-        opening.mVisibleRequested = false; // starts invisible
+        opening.setVisibleRequested(false); // starts invisible
         final Task closeTask = createTask(mDisplayContent);
         final ActivityRecord closing = createActivityRecord(closeTask);
-        closing.mVisibleRequested = true; // starts visible
+        closing.setVisibleRequested(true); // starts visible
 
         transition.collectExistenceChange(openTask);
         transition.collect(opening);
         transition.collect(closing);
-        opening.mVisibleRequested = true;
-        closing.mVisibleRequested = false;
+        opening.setVisibleRequested(true);
+        closing.setVisibleRequested(false);
 
         ArrayList<WindowContainer> targets = Transition.calculateTargets(
                 transition.mParticipants, transition.mChanges);
@@ -323,7 +323,7 @@
                     WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD);
             final ActivityRecord act = createActivityRecord(tasks[i]);
             // alternate so that the transition doesn't get promoted to the display area
-            act.mVisibleRequested = (i % 2) == 0; // starts invisible
+            act.setVisibleRequested((i % 2) == 0); // starts invisible
         }
 
         // doesn't matter which order collected since participants is a set
@@ -331,7 +331,7 @@
             transition.collectExistenceChange(tasks[i]);
             final ActivityRecord act = tasks[i].getTopMostActivity();
             transition.collect(act);
-            tasks[i].getTopMostActivity().mVisibleRequested = (i % 2) != 0;
+            tasks[i].getTopMostActivity().setVisibleRequested((i % 2) != 0);
         }
 
         ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -360,7 +360,7 @@
                     WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
             final ActivityRecord act = createActivityRecord(tasks[i]);
             // alternate so that the transition doesn't get promoted to the display area
-            act.mVisibleRequested = (i % 2) == 0; // starts invisible
+            act.setVisibleRequested((i % 2) == 0); // starts invisible
             act.visibleIgnoringKeyguard = (i % 2) == 0;
             if (i == showWallpaperTask) {
                 doReturn(true).when(act).showWallpaper();
@@ -381,7 +381,7 @@
             transition.collectExistenceChange(tasks[i]);
             final ActivityRecord act = tasks[i].getTopMostActivity();
             transition.collect(act);
-            tasks[i].getTopMostActivity().mVisibleRequested = (i % 2) != 0;
+            tasks[i].getTopMostActivity().setVisibleRequested((i % 2) != 0);
         }
 
         ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -417,9 +417,9 @@
         changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
         fillChangeMap(changes, topTask);
         // End states.
-        showing.mVisibleRequested = true;
-        closing.mVisibleRequested = false;
-        hiding.mVisibleRequested = false;
+        showing.setVisibleRequested(true);
+        closing.setVisibleRequested(false);
+        hiding.setVisibleRequested(false);
 
         participants.add(belowTask);
         participants.add(hiding);
@@ -449,9 +449,9 @@
         changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
         fillChangeMap(changes, topTask);
         // End states.
-        showing.mVisibleRequested = true;
-        opening.mVisibleRequested = true;
-        closing.mVisibleRequested = false;
+        showing.setVisibleRequested(true);
+        opening.setVisibleRequested(true);
+        closing.setVisibleRequested(false);
 
         participants.add(belowTask);
         participants.add(showing);
@@ -531,19 +531,19 @@
     @Test
     public void testOpenActivityInTheSameTaskWithDisplayChange() {
         final ActivityRecord closing = createActivityRecord(mDisplayContent);
-        closing.mVisibleRequested = true;
+        closing.setVisibleRequested(true);
         final Task task = closing.getTask();
         makeTaskOrganized(task);
         final ActivityRecord opening = createActivityRecord(task);
-        opening.mVisibleRequested = false;
+        opening.setVisibleRequested(false);
         makeDisplayAreaOrganized(mDisplayContent.getDefaultTaskDisplayArea(), mDisplayContent);
         final WindowContainer<?>[] wcs = { closing, opening, task, mDisplayContent };
         final Transition transition = createTestTransition(TRANSIT_OPEN);
         for (WindowContainer<?> wc : wcs) {
             transition.collect(wc);
         }
-        closing.mVisibleRequested = false;
-        opening.mVisibleRequested = true;
+        closing.setVisibleRequested(false);
+        opening.setVisibleRequested(true);
         final int newRotation = mDisplayContent.getWindowConfiguration().getRotation() + 1;
         for (WindowContainer<?> wc : wcs) {
             wc.getWindowConfiguration().setRotation(newRotation);
@@ -586,9 +586,9 @@
         changes.put(changeInChange, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
         fillChangeMap(changes, openTask);
         // End states.
-        changeInChange.mVisibleRequested = true;
-        openInOpen.mVisibleRequested = true;
-        openInChange.mVisibleRequested = true;
+        changeInChange.setVisibleRequested(true);
+        openInOpen.setVisibleRequested(true);
+        openInChange.setVisibleRequested(true);
 
         final int transit = transition.mType;
         int flags = 0;
@@ -644,8 +644,8 @@
         changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
         fillChangeMap(changes, newTask);
         // End states.
-        closing.mVisibleRequested = true;
-        opening.mVisibleRequested = true;
+        closing.setVisibleRequested(true);
+        opening.setVisibleRequested(true);
 
         final int transit = transition.mType;
         int flags = 0;
@@ -685,8 +685,8 @@
         changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
         fillChangeMap(changes, newTask);
         // End states.
-        closing.mVisibleRequested = true;
-        opening.mVisibleRequested = true;
+        closing.setVisibleRequested(true);
+        opening.setVisibleRequested(true);
 
         final int transit = transition.mType;
         int flags = 0;
@@ -962,7 +962,7 @@
         home.mTransitionController.requestStartTransition(transition, home.getTask(),
                 null /* remoteTransition */, null /* displayChange */);
         transition.collectExistenceChange(home);
-        home.mVisibleRequested = true;
+        home.setVisibleRequested(true);
         mDisplayContent.setFixedRotationLaunchingAppUnchecked(home);
         doReturn(true).when(home).hasFixedRotationTransform(any());
         player.startTransition();
@@ -998,12 +998,12 @@
         // Start out with task2 visible and set up a transition that closes task2 and opens task1
         final Task task1 = createTask(mDisplayContent);
         final ActivityRecord activity1 = createActivityRecord(task1);
-        activity1.mVisibleRequested = false;
+        activity1.setVisibleRequested(false);
         activity1.setVisible(false);
         final Task task2 = createTask(mDisplayContent);
         makeTaskOrganized(task1, task2);
         final ActivityRecord activity2 = createActivityRecord(task1);
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
         activity2.setVisible(true);
 
         openTransition.collectExistenceChange(task1);
@@ -1011,9 +1011,9 @@
         openTransition.collectExistenceChange(task2);
         openTransition.collectExistenceChange(activity2);
 
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         activity1.setVisible(true);
-        activity2.mVisibleRequested = false;
+        activity2.setVisibleRequested(false);
 
         // Using abort to force-finish the sync (since we can't wait for drawing in unit test).
         // We didn't call abort on the transition itself, so it will still run onTransactionReady
@@ -1029,8 +1029,8 @@
         closeTransition.collectExistenceChange(task2);
         closeTransition.collectExistenceChange(activity2);
 
-        activity1.mVisibleRequested = false;
-        activity2.mVisibleRequested = true;
+        activity1.setVisibleRequested(false);
+        activity2.setVisibleRequested(true);
 
         openTransition.finishTransition();
 
@@ -1072,12 +1072,12 @@
         // Start out with task2 visible and set up a transition that closes task2 and opens task1
         final Task task1 = createTask(mDisplayContent);
         final ActivityRecord activity1 = createActivityRecord(task1);
-        activity1.mVisibleRequested = false;
+        activity1.setVisibleRequested(false);
         activity1.setVisible(false);
         final Task task2 = createTask(mDisplayContent);
         makeTaskOrganized(task1, task2);
         final ActivityRecord activity2 = createActivityRecord(task2);
-        activity2.mVisibleRequested = true;
+        activity2.setVisibleRequested(true);
         activity2.setVisible(true);
 
         openTransition.collectExistenceChange(task1);
@@ -1085,9 +1085,9 @@
         openTransition.collectExistenceChange(task2);
         openTransition.collectExistenceChange(activity2);
 
-        activity1.mVisibleRequested = true;
+        activity1.setVisibleRequested(true);
         activity1.setVisible(true);
-        activity2.mVisibleRequested = false;
+        activity2.setVisibleRequested(false);
 
         // Using abort to force-finish the sync (since we can't wait for drawing in unit test).
         // We didn't call abort on the transition itself, so it will still run onTransactionReady
@@ -1107,8 +1107,8 @@
         closeTransition.collectExistenceChange(activity2);
         closeTransition.setTransientLaunch(activity2, null /* restoreBelow */);
 
-        activity1.mVisibleRequested = false;
-        activity2.mVisibleRequested = true;
+        activity1.setVisibleRequested(false);
+        activity2.setVisibleRequested(true);
         activity2.setVisible(true);
 
         // Using abort to force-finish the sync (since we obviously can't wait for drawing).
@@ -1166,8 +1166,8 @@
         changes.put(activity0, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
         changes.put(activity1, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
         // End states.
-        activity0.mVisibleRequested = false;
-        activity1.mVisibleRequested = true;
+        activity0.setVisibleRequested(false);
+        activity1.setVisibleRequested(true);
 
         participants.add(activity0);
         participants.add(activity1);
@@ -1210,9 +1210,9 @@
         changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(true /* vis */,
                 false /* exChg */));
         // End states.
-        closingActivity.mVisibleRequested = false;
-        openingActivity.mVisibleRequested = true;
-        nonEmbeddedActivity.mVisibleRequested = false;
+        closingActivity.setVisibleRequested(false);
+        openingActivity.setVisibleRequested(true);
+        nonEmbeddedActivity.setVisibleRequested(false);
 
         participants.add(closingActivity);
         participants.add(openingActivity);
@@ -1255,8 +1255,8 @@
                 false /* exChg */));
         changes.put(embeddedTf, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
         // End states.
-        nonEmbeddedActivity.mVisibleRequested = false;
-        embeddedActivity.mVisibleRequested = true;
+        nonEmbeddedActivity.setVisibleRequested(false);
+        embeddedActivity.setVisibleRequested(true);
         embeddedTf.setBounds(new Rect(0, 0, 500, 500));
 
         participants.add(nonEmbeddedActivity);
@@ -1285,11 +1285,11 @@
         final ActivityRecord activity = createActivityRecord(task);
         // Start states: set bounds to make sure the start bounds is ignored if it is not visible.
         activity.getConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 250, 500));
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         changes.put(activity, new Transition.ChangeInfo(activity));
         // End states: reset bounds to fill Task.
         activity.getConfiguration().windowConfiguration.setBounds(taskBounds);
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
 
         participants.add(activity);
         final ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -1313,11 +1313,11 @@
         task.getConfiguration().windowConfiguration.setBounds(taskBounds);
         final ActivityRecord activity = createActivityRecord(task);
         // Start states: fills Task without override.
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         changes.put(activity, new Transition.ChangeInfo(activity));
         // End states: set bounds to make sure the start bounds is ignored if it is not visible.
         activity.getConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 250, 500));
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
 
         participants.add(activity);
         final ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -1340,12 +1340,12 @@
         final Task lastParent = createTask(mDisplayContent);
         final Task newParent = createTask(mDisplayContent);
         final ActivityRecord activity = createActivityRecord(lastParent);
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         // Skip manipulate the SurfaceControl.
         doNothing().when(activity).setDropInputMode(anyInt());
         changes.put(activity, new Transition.ChangeInfo(activity));
         activity.reparent(newParent, POSITION_TOP);
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
 
         participants.add(activity);
         final ArrayList<WindowContainer> targets = Transition.calculateTargets(
@@ -1365,7 +1365,7 @@
         final Task task = createTask(mDisplayContent);
         task.setBounds(new Rect(0, 0, 2000, 1000));
         final ActivityRecord activity = createActivityRecord(task);
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         // Skip manipulate the SurfaceControl.
         doNothing().when(activity).setDropInputMode(anyInt());
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
@@ -1413,13 +1413,13 @@
         task.setTaskDescription(taskDescription);
 
         // Start states:
-        embeddedActivity.mVisibleRequested = true;
-        nonEmbeddedActivity.mVisibleRequested = false;
+        embeddedActivity.setVisibleRequested(true);
+        nonEmbeddedActivity.setVisibleRequested(false);
         changes.put(embeddedTf, new Transition.ChangeInfo(embeddedTf));
         changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(nonEmbeddedActivity));
         // End states:
-        embeddedActivity.mVisibleRequested = false;
-        nonEmbeddedActivity.mVisibleRequested = true;
+        embeddedActivity.setVisibleRequested(false);
+        nonEmbeddedActivity.setVisibleRequested(true);
 
         participants.add(embeddedTf);
         participants.add(nonEmbeddedActivity);
@@ -1532,7 +1532,7 @@
         final ActivityRecord activity = createActivityRecord(lastParent);
         doReturn(true).when(lastParent).shouldRemoveSelfOnLastChildRemoval();
         doNothing().when(activity).setDropInputMode(anyInt());
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
 
         final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */,
                 activity.mTransitionController, mWm.mSyncEngine);
diff --git a/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
index 45e1141..2fccb88a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/UnknownAppVisibilityControllerTest.java
@@ -95,7 +95,7 @@
         final ActivityRecord activity = createNonAttachedActivityRecord(mDisplayContent);
         mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(activity);
         activity.finishing = true;
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         activity.setVisibility(false, false);
         assertTrue(mDisplayContent.mUnknownAppVisibilityController.allResolved());
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 94b5b93..a100b9a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -313,12 +313,12 @@
         r.applyFixedRotationTransform(mDisplayContent.getDisplayInfo(),
                 mDisplayContent.mDisplayFrames, mDisplayContent.getConfiguration());
         // Invisible requested activity should not share its rotation transform.
-        r.mVisibleRequested = false;
+        r.setVisibleRequested(false);
         mDisplayContent.mWallpaperController.adjustWallpaperWindows();
         assertFalse(wallpaperToken.hasFixedRotationTransform());
 
         // Wallpaper should link the transform of its target.
-        r.mVisibleRequested = true;
+        r.setVisibleRequested(true);
         mDisplayContent.mWallpaperController.adjustWallpaperWindows();
         assertEquals(appWin, mDisplayContent.mWallpaperController.getWallpaperTarget());
         assertTrue(r.hasFixedRotationTransform());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 871030f..3d777f8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -205,7 +205,7 @@
         win.mViewVisibility = View.VISIBLE;
         win.mHasSurface = true;
         win.mActivityRecord.mAppStopped = true;
-        win.mActivityRecord.mVisibleRequested = false;
+        win.mActivityRecord.setVisibleRequested(false);
         win.mActivityRecord.setVisible(false);
         mWm.mWindowMap.put(win.mClient.asBinder(), win);
         final int w = 100;
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index c73e237..e5e9f54 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -992,7 +992,7 @@
         final Task task = createTask(rootTaskController1);
         final WindowState w = createAppWindow(task, TYPE_APPLICATION, "Enlightened Window");
 
-        w.mActivityRecord.mVisibleRequested = true;
+        w.mActivityRecord.setVisibleRequested(true);
         w.mActivityRecord.setVisible(true);
 
         BLASTSyncEngine bse = new BLASTSyncEngine(mWm);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index 3abf7ce..8bd4148 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -324,7 +324,7 @@
     @Test
     public void testComputeOomAdjFromActivities() {
         final ActivityRecord activity = createActivityRecord(mWpc);
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         final int[] callbackResult = { 0 };
         final int visible = 1;
         final int paused = 2;
@@ -359,7 +359,7 @@
         assertEquals(visible, callbackResult[0]);
 
         callbackResult[0] = 0;
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.setState(PAUSED, "test");
         mWpc.computeOomAdjFromActivities(callback);
         assertEquals(paused, callbackResult[0]);
@@ -380,7 +380,7 @@
         final VisibleActivityProcessTracker tracker = mAtm.mVisibleActivityProcessTracker;
         spyOn(tracker);
         final ActivityRecord activity = createActivityRecord(mWpc);
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         activity.setState(STARTED, "test");
 
         verify(tracker).onAnyActivityVisible(mWpc);
@@ -398,7 +398,7 @@
         assertTrue(mWpc.hasForegroundActivities());
 
         activity.setVisibility(false);
-        activity.mVisibleRequested = false;
+        activity.setVisibleRequested(false);
         activity.setState(STOPPED, "test");
 
         verify(tracker).onAllActivitiesInvisible(mWpc);
@@ -413,7 +413,7 @@
     @Test
     public void testTopActivityUiModeChangeScheduleConfigChange() {
         final ActivityRecord activity = createActivityRecord(mWpc);
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         doReturn(true).when(activity).applyAppSpecificConfig(anyInt(), any());
         mWpc.updateAppSpecificSettingsForAllActivitiesInPackage(DEFAULT_COMPONENT_PACKAGE_NAME,
                 Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"));
@@ -423,7 +423,7 @@
     @Test
     public void testTopActivityUiModeChangeForDifferentPackage_noScheduledConfigChange() {
         final ActivityRecord activity = createActivityRecord(mWpc);
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
         mWpc.updateAppSpecificSettingsForAllActivitiesInPackage("com.different.package",
                 Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"));
         verify(activity, never()).applyAppSpecificConfig(anyInt(), any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 1b79dd3..435c39c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -264,7 +264,7 @@
 
         // Verify that app window can still be IME target as long as it is visible (even if
         // it is going to become invisible).
-        appWindow.mActivityRecord.mVisibleRequested = false;
+        appWindow.mActivityRecord.setVisibleRequested(false);
         assertTrue(appWindow.canBeImeTarget());
 
         // Make windows invisible
@@ -720,7 +720,7 @@
 
         // No need to wait for a window of invisible activity even if the window has surface.
         final WindowState invisibleApp = mAppWindow;
-        invisibleApp.mActivityRecord.mVisibleRequested = false;
+        invisibleApp.mActivityRecord.setVisibleRequested(false);
         invisibleApp.mActivityRecord.allDrawn = false;
         outWaitingForDrawn.clear();
         invisibleApp.requestDrawIfNeeded(outWaitingForDrawn);
@@ -738,7 +738,7 @@
         assertFalse(startingApp.getOrientationChanging());
 
         // Even if the display is frozen, invisible requested window should not be affected.
-        startingApp.mActivityRecord.mVisibleRequested = false;
+        startingApp.mActivityRecord.setVisibleRequested(false);
         mWm.startFreezingDisplay(0, 0, mDisplayContent);
         doReturn(true).when(mWm.mPolicy).isScreenOn();
         startingApp.getWindowFrames().setInsetsChanged(true);
@@ -813,7 +813,7 @@
         final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity,
                 "App window");
         doReturn(true).when(embeddedActivity).isVisible();
-        embeddedActivity.mVisibleRequested = true;
+        embeddedActivity.setVisibleRequested(true);
         makeWindowVisible(win);
         win.mLayoutSeq = win.getDisplayContent().mLayoutSeq;
         // Set the bounds twice:
@@ -838,7 +838,7 @@
     @Test
     public void testCantReceiveTouchWhenAppTokenHiddenRequested() {
         final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
-        win0.mActivityRecord.mVisibleRequested = false;
+        win0.mActivityRecord.setVisibleRequested(false);
         assertFalse(win0.canReceiveTouchInput());
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 5a261bc65..019b14d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -738,7 +738,7 @@
         activity.onDisplayChanged(dc);
         activity.setOccludesParent(true);
         activity.setVisible(true);
-        activity.mVisibleRequested = true;
+        activity.setVisibleRequested(true);
     }
 
     /**
@@ -1240,7 +1240,7 @@
                     mTask.moveToFront("createActivity");
                 }
                 if (mVisible) {
-                    activity.mVisibleRequested = true;
+                    activity.setVisibleRequested(true);
                     activity.setVisible(true);
                 }
             }
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java
new file mode 100644
index 0000000..e82a7c2
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.wm.utils;
+
+import static com.android.server.wm.utils.StateMachine.isIn;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Build/Install/Run:
+ *  atest WmTests:StateMachineTest
+ */
+@SmallTest
+@Presubmit
+public class StateMachineTest {
+    static class LoggingHandler implements StateMachine.Handler {
+        final int mState;
+        final StringBuffer mStringBuffer;
+        // True if process #handle
+        final boolean mHandleSelf;
+
+        LoggingHandler(int state, StringBuffer sb, boolean handleSelf) {
+            mHandleSelf = handleSelf;
+            mState = state;
+            mStringBuffer = sb;
+        }
+
+        LoggingHandler(int state, StringBuffer sb) {
+            this(state, sb, true /* handleSelf */);
+        }
+
+        @Override
+        public void enter() {
+            mStringBuffer.append('i');
+            mStringBuffer.append(Integer.toHexString(mState));
+            mStringBuffer.append(';');
+        }
+
+        @Override
+        public void exit() {
+            mStringBuffer.append('o');
+            mStringBuffer.append(Integer.toHexString(mState));
+            mStringBuffer.append(';');
+        }
+
+        @Override
+        public boolean handle(int event, Object param) {
+            if (mHandleSelf) {
+                mStringBuffer.append('h');
+                mStringBuffer.append(Integer.toHexString(mState));
+                mStringBuffer.append(';');
+            }
+            return mHandleSelf;
+        }
+    }
+
+    static class LoggingHandlerTransferInExit extends LoggingHandler {
+        final StateMachine mStateMachine;
+        final int mStateToTransit;
+
+        LoggingHandlerTransferInExit(int state, StringBuffer sb, StateMachine stateMachine,
+                int stateToTransit) {
+            super(state, sb);
+            mStateMachine = stateMachine;
+            mStateToTransit = stateToTransit;
+        }
+
+        @Override
+        public void exit() {
+            super.exit();
+            mStateMachine.transit(mStateToTransit);
+        }
+    }
+
+    @Test
+    public void testStateMachineIsIn() {
+        assertTrue(isIn(0x112, 0x1));
+        assertTrue(isIn(0x112, 0x11));
+        assertTrue(isIn(0x112, 0x112));
+
+        assertFalse(isIn(0x1, 0x112));
+        assertFalse(isIn(0x12, 0x2));
+    }
+
+    @Test
+    public void testStateMachineInitialState() {
+        StateMachine stateMachine = new StateMachine();
+        assertEquals(0, stateMachine.getState());
+
+        stateMachine = new StateMachine(0x23);
+        assertEquals(0x23, stateMachine.getState());
+    }
+
+    @Test
+    public void testStateMachineTransitToChild() {
+        final StringBuffer log = new StringBuffer();
+
+        StateMachine stateMachine = new StateMachine();
+        stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log));
+        stateMachine.addStateHandler(0x12, new LoggingHandler(0x12, log));
+        stateMachine.addStateHandler(0x123, new LoggingHandler(0x123, log));
+        stateMachine.addStateHandler(0x1233, new LoggingHandler(0x1233, log));
+
+        // 0x0 -> 0x12
+        stateMachine.transit(0x12);
+        assertEquals("i1;i12;", log.toString());
+        assertEquals(0x12, stateMachine.getState());
+
+        // 0x12 -> 0x1233
+        log.setLength(0);
+        stateMachine.transit(0x1233);
+        assertEquals(0x1233, stateMachine.getState());
+        assertEquals("i123;i1233;", log.toString());
+    }
+
+    @Test
+    public void testStateMachineTransitToParent() {
+        final StringBuffer log = new StringBuffer();
+
+        StateMachine stateMachine = new StateMachine(0x253);
+        stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log));
+        stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log));
+        stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log));
+
+        // 0x253 -> 0x2
+        stateMachine.transit(0x2);
+        assertEquals(0x2, stateMachine.getState());
+        assertEquals("o253;o25;", log.toString());
+    }
+
+    @Test
+    public void testStateMachineTransitSelf() {
+        final StringBuffer log = new StringBuffer();
+
+        StateMachine stateMachine = new StateMachine(0x253);
+        stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log));
+        stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log));
+        stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log));
+
+        // 0x253 -> 0x253
+        stateMachine.transit(0x253);
+        assertEquals(0x253, stateMachine.getState());
+        assertEquals("o253;i253;", log.toString());
+    }
+
+    @Test
+    public void testStateMachineTransitGeneral() {
+        final StringBuffer log = new StringBuffer();
+
+        StateMachine stateMachine = new StateMachine(0x1351);
+        stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log));
+        stateMachine.addStateHandler(0x13, new LoggingHandler(0x13, log));
+        stateMachine.addStateHandler(0x132, new LoggingHandler(0x132, log));
+        stateMachine.addStateHandler(0x1322, new LoggingHandler(0x1322, log));
+        stateMachine.addStateHandler(0x1322, new LoggingHandler(0x1322, log));
+        stateMachine.addStateHandler(0x135, new LoggingHandler(0x135, log));
+        stateMachine.addStateHandler(0x1351, new LoggingHandler(0x1351, log));
+
+        // 0x1351 -> 0x1322
+        // least common ancestor = 0x13
+        stateMachine.transit(0x1322);
+        assertEquals(0x1322, stateMachine.getState());
+        assertEquals("o1351;o135;i132;i1322;", log.toString());
+    }
+
+    @Test
+    public void testStateMachineTriggerStateAction() {
+        final StringBuffer log = new StringBuffer();
+
+        StateMachine stateMachine = new StateMachine(0x253);
+        stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log));
+        stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log));
+        stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log));
+
+        // state 0x253 handles the message itself
+        stateMachine.handle(0, null);
+        assertEquals("h253;", log.toString());
+    }
+
+    @Test
+    public void testStateMachineTriggerStateActionDelegate() {
+        final StringBuffer log = new StringBuffer();
+
+        StateMachine stateMachine = new StateMachine(0x253);
+        stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log));
+        stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log));
+        stateMachine.addStateHandler(0x253,
+                new LoggingHandler(0x253, log, false /* handleSelf */));
+
+        // state 0x253 delegate the message handling to its parent state
+        stateMachine.handle(0, null);
+        assertEquals("h25;", log.toString());
+    }
+
+    @Test
+    public void testStateMachineNestedTransition() {
+        final StringBuffer log = new StringBuffer();
+
+        StateMachine stateMachine = new StateMachine(0x25);
+        stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log));
+
+        // Force transit to state 0x3 in exit()
+        stateMachine.addStateHandler(0x2,
+                new LoggingHandlerTransferInExit(0x2, log, stateMachine, 0x3));
+        stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log));
+        stateMachine.addStateHandler(0x3, new LoggingHandler(0x3, log));
+
+        stateMachine.transit(0x1);
+        // Start transit to 0x1
+        //  0x25 -> 0x2 [transit(0x3) requested] -> 0x1
+        //  0x1 -> 0x3
+        // Immediately set the status to 0x1, no enter/exit
+        assertEquals("o25;o2;i1;o1;i3;", log.toString());
+    }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 4fd2b78..b3a1f2b 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1864,6 +1864,21 @@
                         mResponseStatsTracker.dump(idpw);
                     }
                     return;
+                } else if ("app-component-usage".equals(arg)) {
+                    final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+                    synchronized (mLock) {
+                        if (!mLastTimeComponentUsedGlobal.isEmpty()) {
+                            ipw.println("App Component Usages:");
+                            ipw.increaseIndent();
+                            for (String pkg : mLastTimeComponentUsedGlobal.keySet()) {
+                                ipw.println("package=" + pkg
+                                            + " lastUsed=" + UserUsageStatsService.formatDateTime(
+                                                    mLastTimeComponentUsedGlobal.get(pkg), true));
+                            }
+                            ipw.decreaseIndent();
+                        }
+                    }
+                    return;
                 } else if (arg != null && !arg.startsWith("-")) {
                     // Anything else that doesn't start with '-' is a pkg to filter
                     pkgs.add(arg);
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index ffdb07b..b6aed2db 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -219,7 +219,7 @@
 
     private static EventLogger sEventLogger;
 
-    private UsbGadgetHal mUsbGadgetHal;
+    private static UsbGadgetHal mUsbGadgetHal;
 
     /**
      * Counter for tracking UsbOperation operations.
@@ -1976,7 +1976,7 @@
         }
     }
 
-    private final class UsbHandlerHal extends UsbHandler {
+    private static final class UsbHandlerHal extends UsbHandler {
 
         private final Object mGadgetProxyLock = new Object();
 
diff --git a/telephony/java/android/service/euicc/EuiccProfileInfo.java b/telephony/java/android/service/euicc/EuiccProfileInfo.java
index 8ec500b..7eccd1a 100644
--- a/telephony/java/android/service/euicc/EuiccProfileInfo.java
+++ b/telephony/java/android/service/euicc/EuiccProfileInfo.java
@@ -49,7 +49,6 @@
             POLICY_RULE_DO_NOT_DELETE,
             POLICY_RULE_DELETE_AFTER_DISABLING
     })
-    /** @hide */
     public @interface PolicyRule {}
     /** Once this profile is enabled, it cannot be disabled. */
     public static final int POLICY_RULE_DO_NOT_DISABLE = 1;
@@ -66,7 +65,6 @@
             PROFILE_CLASS_OPERATIONAL,
             PROFILE_CLASS_UNSET
     })
-    /** @hide */
     public @interface ProfileClass {}
     /** Testing profiles */
     public static final int PROFILE_CLASS_TESTING = 0;
@@ -87,7 +85,6 @@
             PROFILE_STATE_ENABLED,
             PROFILE_STATE_UNSET
     })
-    /** @hide */
     public @interface ProfileState {}
     /** Disabled profiles */
     public static final int PROFILE_STATE_DISABLED = 0;
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index 6be2f77..7c600b1 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -138,6 +138,12 @@
      */
     public static final int FREQUENCY_RANGE_MMWAVE = 4;
 
+    /**
+     * Number of frequency ranges.
+     * @hide
+     */
+    public static final int FREQUENCY_RANGE_COUNT = 5;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = "DUPLEX_MODE_",
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index db9dfbb..3b84b65 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -649,6 +649,15 @@
     }
 
     /**
+     * @return {@code true} if the subscription is from the actively used SIM.
+     *
+     * @hide
+     */
+    public boolean isActive() {
+        return mSimSlotIndex >= 0 || mType == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM;
+    }
+
+    /**
      * Used in scenarios where different subscriptions are bundled as a group.
      * It's typically a primary and an opportunistic subscription. (see {@link #isOpportunistic()})
      * Such that those subscriptions will have some affiliated behaviors such as opportunistic
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 5c1d497..5244f41 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -155,6 +155,10 @@
     private static final String CACHE_KEY_SLOT_INDEX_PROPERTY =
             "cache_key.telephony.get_slot_index";
 
+    /** The IPC cache key shared by all subscription manager service cacheable properties. */
+    private static final String CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY =
+            "cache_key.telephony.subscription_manager_service";
+
     /** @hide */
     public static final String GET_SIM_SPECIFIC_SETTINGS_METHOD_NAME = "getSimSpecificSettings";
 
@@ -269,37 +273,72 @@
                     CACHE_KEY_DEFAULT_SUB_ID_PROPERTY,
                     INVALID_SUBSCRIPTION_ID);
 
+    private static VoidPropertyInvalidatedCache<Integer> sGetDefaultSubIdCache =
+            new VoidPropertyInvalidatedCache<>(ISub::getDefaultSubId,
+                    CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
+                    INVALID_SUBSCRIPTION_ID);
+
     private static VoidPropertyInvalidatedCache<Integer> sDefaultDataSubIdCache =
             new VoidPropertyInvalidatedCache<>(ISub::getDefaultDataSubId,
                     CACHE_KEY_DEFAULT_DATA_SUB_ID_PROPERTY,
                     INVALID_SUBSCRIPTION_ID);
 
+    private static VoidPropertyInvalidatedCache<Integer> sGetDefaultDataSubIdCache =
+            new VoidPropertyInvalidatedCache<>(ISub::getDefaultDataSubId,
+                    CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
+                    INVALID_SUBSCRIPTION_ID);
+
     private static VoidPropertyInvalidatedCache<Integer> sDefaultSmsSubIdCache =
             new VoidPropertyInvalidatedCache<>(ISub::getDefaultSmsSubId,
                     CACHE_KEY_DEFAULT_SMS_SUB_ID_PROPERTY,
                     INVALID_SUBSCRIPTION_ID);
 
+    private static VoidPropertyInvalidatedCache<Integer> sGetDefaultSmsSubIdCache =
+            new VoidPropertyInvalidatedCache<>(ISub::getDefaultSmsSubId,
+                    CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
+                    INVALID_SUBSCRIPTION_ID);
+
     private static VoidPropertyInvalidatedCache<Integer> sActiveDataSubIdCache =
             new VoidPropertyInvalidatedCache<>(ISub::getActiveDataSubscriptionId,
                     CACHE_KEY_ACTIVE_DATA_SUB_ID_PROPERTY,
                     INVALID_SUBSCRIPTION_ID);
 
+    private static VoidPropertyInvalidatedCache<Integer> sGetActiveDataSubscriptionIdCache =
+            new VoidPropertyInvalidatedCache<>(ISub::getActiveDataSubscriptionId,
+                    CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
+                    INVALID_SUBSCRIPTION_ID);
+
     private static IntegerPropertyInvalidatedCache<Integer> sSlotIndexCache =
             new IntegerPropertyInvalidatedCache<>(ISub::getSlotIndex,
                     CACHE_KEY_SLOT_INDEX_PROPERTY,
                     INVALID_SIM_SLOT_INDEX);
 
+    private static IntegerPropertyInvalidatedCache<Integer> sGetSlotIndexCache =
+            new IntegerPropertyInvalidatedCache<>(ISub::getSlotIndex,
+                    CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
+                    INVALID_SIM_SLOT_INDEX);
+
     private static IntegerPropertyInvalidatedCache<Integer> sSubIdCache =
             new IntegerPropertyInvalidatedCache<>(ISub::getSubId,
                     CACHE_KEY_SLOT_INDEX_PROPERTY,
                     INVALID_SUBSCRIPTION_ID);
 
+    private static IntegerPropertyInvalidatedCache<Integer> sGetSubIdCache =
+            new IntegerPropertyInvalidatedCache<>(ISub::getSubId,
+                    CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
+                    INVALID_SUBSCRIPTION_ID);
+
     /** Cache depends on getDefaultSubId, so we use the defaultSubId cache key */
     private static IntegerPropertyInvalidatedCache<Integer> sPhoneIdCache =
             new IntegerPropertyInvalidatedCache<>(ISub::getPhoneId,
                     CACHE_KEY_DEFAULT_SUB_ID_PROPERTY,
                     INVALID_PHONE_INDEX);
 
+    private static IntegerPropertyInvalidatedCache<Integer> sGetPhoneIdCache =
+            new IntegerPropertyInvalidatedCache<>(ISub::getPhoneId,
+                    CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY,
+                    INVALID_PHONE_INDEX);
+
     /**
      * Generates a content {@link Uri} used to receive updates on simInfo change
      * on the given subscriptionId
@@ -1298,6 +1337,8 @@
 
     private final Context mContext;
 
+    private static boolean sIsSubscriptionManagerServiceEnabled = false;
+
     // Cache of Resource that has been created in getResourcesForSubId. Key is a Pair containing
     // the Context and subId.
     private static final Map<Pair<Context, Integer>, Resources> sResourcesCache =
@@ -1383,6 +1424,19 @@
     public SubscriptionManager(Context context) {
         if (DBG) logd("SubscriptionManager created");
         mContext = context;
+
+        sIsSubscriptionManagerServiceEnabled = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_using_subscription_manager_service);
+    }
+
+    /**
+     * @return {@code true} if the new subscription manager service is used. This is temporary and
+     * will be removed before Android 14 release.
+     *
+     * @hide
+     */
+    public static boolean isSubscriptionManagerServiceEnabled() {
+        return sIsSubscriptionManagerServiceEnabled;
     }
 
     private NetworkPolicyManager getNetworkPolicyManager() {
@@ -1712,8 +1766,7 @@
      *
      * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
      * or that the calling app has carrier privileges (see
-     * {@link TelephonyManager#hasCarrierPrivileges}). In the latter case, only records accessible
-     * to the calling app are returned.
+     * {@link TelephonyManager#hasCarrierPrivileges}).
      *
      * @return Sorted list of the currently {@link SubscriptionInfo} records available on the device.
      * <ul>
@@ -1731,7 +1784,6 @@
      * </li>
      * </ul>
      */
-    @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
     public List<SubscriptionInfo> getActiveSubscriptionInfoList() {
         return getActiveSubscriptionInfoList(/* userVisibleonly */true);
@@ -1935,17 +1987,12 @@
     }
 
     /**
+     * Get the active subscription count.
      *
-     * Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
-     * or that the calling app has carrier privileges (see
-     * {@link TelephonyManager#hasCarrierPrivileges}). In the latter case, the count will include
-     * only those subscriptions accessible to the caller.
+     * @return The current number of active subscriptions.
      *
-     * @return the current number of active subscriptions. There is no guarantee the value
-     * returned by this method will be the same as the length of the list returned by
-     * {@link #getActiveSubscriptionInfoList}.
+     * @see #getActiveSubscriptionInfoList()
      */
-    @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
     public int getActiveSubscriptionInfoCount() {
         int result = 0;
@@ -2085,7 +2132,7 @@
     /**
      * Set SIM icon tint color for subscription ID
      * @param tint the RGB value of icon tint color of the SIM
-     * @param subId the unique Subscritpion ID in database
+     * @param subId the unique subscription ID in database
      * @return the number of records updated
      * @hide
      */
@@ -2093,7 +2140,7 @@
     public int setIconTint(@ColorInt int tint, int subId) {
         if (VDBG) logd("[setIconTint]+ tint:" + tint + " subId:" + subId);
         return setSubscriptionPropertyHelper(subId, "setIconTint",
-                (iSub)-> iSub.setIconTint(tint, subId)
+                (iSub)-> iSub.setIconTint(subId, tint)
         );
     }
 
@@ -2158,6 +2205,7 @@
      * subscriptionId doesn't have an associated slot index.
      */
     public static int getSlotIndex(int subscriptionId) {
+        if (isSubscriptionManagerServiceEnabled()) return sGetSlotIndexCache.query(subscriptionId);
         return sSlotIndexCache.query(subscriptionId);
     }
 
@@ -2207,12 +2255,14 @@
             return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
         }
 
+        if (isSubscriptionManagerServiceEnabled()) return sGetSubIdCache.query(slotIndex);
         return sSubIdCache.query(slotIndex);
     }
 
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     public static int getPhoneId(int subId) {
+        if (isSubscriptionManagerServiceEnabled()) return sGetPhoneIdCache.query(subId);
         return sPhoneIdCache.query(subId);
     }
 
@@ -2234,6 +2284,7 @@
      * @return the "system" default subscription id.
      */
     public static int getDefaultSubscriptionId() {
+        if (isSubscriptionManagerServiceEnabled()) return sGetDefaultSubIdCache.query(null);
         return sDefaultSubIdCache.query(null);
     }
 
@@ -2322,6 +2373,7 @@
      * @return the default SMS subscription Id.
      */
     public static int getDefaultSmsSubscriptionId() {
+        if (isSubscriptionManagerServiceEnabled()) return sGetDefaultSmsSubIdCache.query(null);
         return sDefaultSmsSubIdCache.query(null);
     }
 
@@ -2356,6 +2408,7 @@
      * @return the default data subscription Id.
      */
     public static int getDefaultDataSubscriptionId() {
+        if (isSubscriptionManagerServiceEnabled()) return sGetDefaultDataSubIdCache.query(null);
         return sDefaultDataSubIdCache.query(null);
     }
 
@@ -3819,6 +3872,9 @@
      * SubscriptionManager.INVALID_SUBSCRIPTION_ID if not.
      */
     public static int getActiveDataSubscriptionId() {
+        if (isSubscriptionManagerServiceEnabled()) {
+            return sGetActiveDataSubscriptionIdCache.query(null);
+        }
         return sActiveDataSubIdCache.query(null);
     }
 
@@ -3862,6 +3918,11 @@
         PropertyInvalidatedCache.invalidateCache(CACHE_KEY_SLOT_INDEX_PROPERTY);
     }
 
+    /** @hide */
+    public static void invalidateSubscriptionManagerServiceCaches() {
+        PropertyInvalidatedCache.invalidateCache(CACHE_KEY_SUBSCRIPTION_MANAGER_SERVICE_PROPERTY);
+    }
+
     /**
      * Allows a test process to disable client-side caching operations.
      *
@@ -3873,7 +3934,16 @@
         sActiveDataSubIdCache.disableLocal();
         sDefaultSmsSubIdCache.disableLocal();
         sSlotIndexCache.disableLocal();
+        sSubIdCache.disableLocal();
         sPhoneIdCache.disableLocal();
+
+        sGetDefaultSubIdCache.disableLocal();
+        sGetDefaultDataSubIdCache.disableLocal();
+        sGetActiveDataSubscriptionIdCache.disableLocal();
+        sGetDefaultSmsSubIdCache.disableLocal();
+        sGetSlotIndexCache.disableLocal();
+        sGetSubIdCache.disableLocal();
+        sGetPhoneIdCache.disableLocal();
     }
 
     /**
@@ -3886,7 +3956,16 @@
         sActiveDataSubIdCache.clear();
         sDefaultSmsSubIdCache.clear();
         sSlotIndexCache.clear();
+        sSubIdCache.clear();
         sPhoneIdCache.clear();
+
+        sGetDefaultSubIdCache.clear();
+        sGetDefaultDataSubIdCache.clear();
+        sGetActiveDataSubscriptionIdCache.clear();
+        sGetDefaultSmsSubIdCache.clear();
+        sGetSlotIndexCache.clear();
+        sGetSubIdCache.clear();
+        sGetPhoneIdCache.clear();
     }
 
     /**
diff --git a/telephony/java/android/telephony/euicc/EuiccCardManager.java b/telephony/java/android/telephony/euicc/EuiccCardManager.java
index e61d1e6..b18eaa53 100644
--- a/telephony/java/android/telephony/euicc/EuiccCardManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccCardManager.java
@@ -75,7 +75,6 @@
             CANCEL_REASON_TIMEOUT,
             CANCEL_REASON_PPR_NOT_ALLOWED
     })
-    /** @hide */
     public @interface CancelReason {
     }
 
@@ -105,7 +104,6 @@
             RESET_OPTION_DELETE_FIELD_LOADED_TEST_PROFILES,
             RESET_OPTION_RESET_DEFAULT_SMDP_ADDRESS
     })
-    /** @hide */
     public @interface ResetOption {
     }
 
diff --git a/telephony/java/android/telephony/euicc/EuiccNotification.java b/telephony/java/android/telephony/euicc/EuiccNotification.java
index c348cff..be0048f 100644
--- a/telephony/java/android/telephony/euicc/EuiccNotification.java
+++ b/telephony/java/android/telephony/euicc/EuiccNotification.java
@@ -44,7 +44,6 @@
             EVENT_DISABLE,
             EVENT_DELETE
     })
-    /** @hide */
     public @interface Event {}
 
     /** A profile is downloaded and installed. */
diff --git a/telephony/java/android/telephony/euicc/EuiccRulesAuthTable.java b/telephony/java/android/telephony/euicc/EuiccRulesAuthTable.java
index d5a05ae..1c6b6b6 100644
--- a/telephony/java/android/telephony/euicc/EuiccRulesAuthTable.java
+++ b/telephony/java/android/telephony/euicc/EuiccRulesAuthTable.java
@@ -42,7 +42,6 @@
     @IntDef(flag = true, prefix = { "POLICY_RULE_FLAG_" }, value = {
             POLICY_RULE_FLAG_CONSENT_REQUIRED
     })
-    /** @hide */
     public @interface PolicyRuleFlag {}
 
     /** User consent is required to install the profile. */
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 5173405..e9cea68 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -135,11 +135,11 @@
 
     /**
      * Set SIM icon tint color by simInfo index
-     * @param tint the icon tint color of the SIM
      * @param subId the unique SubscriptionInfo index in database
+     * @param tint the icon tint color of the SIM
      * @return the number of records updated
      */
-    int setIconTint(int tint, int subId);
+    int setIconTint(int subId, int tint);
 
     /**
      * Set display name by simInfo index with name source
diff --git a/tests/FlickerTests/AndroidTest.xml b/tests/FlickerTests/AndroidTest.xml
index a7d6a01..84781b4 100644
--- a/tests/FlickerTests/AndroidTest.xml
+++ b/tests/FlickerTests/AndroidTest.xml
@@ -19,6 +19,8 @@
         <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
         <!-- restart launcher to activate TAPL -->
         <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
+        <!-- Ensure output directory is empty at the start -->
+        <option name="run-command" value="rm -rf /sdcard/flicker" />
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true"/>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
index 3f6a75d..c100a9c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -20,7 +20,7 @@
 import android.platform.test.annotations.Presubmit
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.junit.FlickerBuilderProvider
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import org.junit.Assume
@@ -34,12 +34,12 @@
 abstract class BaseTest
 @JvmOverloads
 constructor(
-    protected val testSpec: FlickerTestParameter,
+    protected val flicker: FlickerTest,
     protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
     protected val tapl: LauncherInstrumentation = LauncherInstrumentation()
 ) {
     init {
-        testSpec.setIsTablet(
+        flicker.scenario.setIsTablet(
             WindowManagerStateHelper(instrumentation, clearCacheAfterParsing = false)
                 .currentState
                 .wmState
@@ -58,13 +58,13 @@
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
-            setup { testSpec.setIsTablet(wmHelper.currentState.wmState.isTablet) }
+            setup { flicker.scenario.setIsTablet(wmHelper.currentState.wmState.isTablet) }
             transition()
         }
     }
 
     /** Checks that all parts of the screen are covered during the transition */
-    @Presubmit @Test open fun entireScreenCovered() = testSpec.entireScreenCovered()
+    @Presubmit @Test open fun entireScreenCovered() = flicker.entireScreenCovered()
 
     /**
      * Checks that the [ComponentNameMatcher.NAV_BAR] layer is visible during the whole transition
@@ -74,8 +74,8 @@
     @Presubmit
     @Test
     open fun navBarLayerIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarLayerIsVisibleAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerIsVisibleAtStartAndEnd()
     }
 
     /**
@@ -87,8 +87,8 @@
     @Presubmit
     @Test
     open fun navBarLayerPositionAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarLayerPositionAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerPositionAtStartAndEnd()
     }
 
     /**
@@ -99,8 +99,8 @@
     @Presubmit
     @Test
     open fun navBarWindowIsAlwaysVisible() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarWindowIsAlwaysVisible()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsAlwaysVisible()
     }
 
     /**
@@ -112,8 +112,8 @@
     @Presubmit
     @Test
     open fun taskBarLayerIsVisibleAtStartAndEnd() {
-        Assume.assumeTrue(testSpec.isTablet)
-        testSpec.taskBarLayerIsVisibleAtStartAndEnd()
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.taskBarLayerIsVisibleAtStartAndEnd()
     }
 
     /**
@@ -124,8 +124,8 @@
     @Presubmit
     @Test
     open fun taskBarWindowIsAlwaysVisible() {
-        Assume.assumeTrue(testSpec.isTablet)
-        testSpec.taskBarWindowIsAlwaysVisible()
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.taskBarWindowIsAlwaysVisible()
     }
 
     /**
@@ -134,8 +134,7 @@
      */
     @Presubmit
     @Test
-    open fun statusBarLayerIsVisibleAtStartAndEnd() =
-        testSpec.statusBarLayerIsVisibleAtStartAndEnd()
+    open fun statusBarLayerIsVisibleAtStartAndEnd() = flicker.statusBarLayerIsVisibleAtStartAndEnd()
 
     /**
      * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
@@ -143,7 +142,7 @@
      */
     @Presubmit
     @Test
-    open fun statusBarLayerPositionAtStartAndEnd() = testSpec.statusBarLayerPositionAtStartAndEnd()
+    open fun statusBarLayerPositionAtStartAndEnd() = flicker.statusBarLayerPositionAtStartAndEnd()
 
     /**
      * Checks that the [ComponentNameMatcher.STATUS_BAR] window is visible during the whole
@@ -151,7 +150,7 @@
      */
     @Presubmit
     @Test
-    open fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible()
+    open fun statusBarWindowIsAlwaysVisible() = flicker.statusBarWindowIsAlwaysVisible()
 
     /**
      * Checks that all layers that are visible on the trace, are visible for at least 2 consecutive
@@ -160,7 +159,7 @@
     @Presubmit
     @Test
     open fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry() }
+        flicker.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry() }
     }
 
     /**
@@ -170,7 +169,7 @@
     @Presubmit
     @Test
     open fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertWm { this.visibleWindowsShownMoreThanOneConsecutiveEntry() }
+        flicker.assertWm { this.visibleWindowsShownMoreThanOneConsecutiveEntry() }
     }
 
     open fun cujCompleted() {
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 bbffd08..f9a245a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
@@ -27,7 +27,7 @@
  * Checks that [ComponentNameMatcher.STATUS_BAR] window is visible and above the app windows in all
  * WM trace entries
  */
-fun FlickerTestParameter.statusBarWindowIsAlwaysVisible() {
+fun FlickerTest.statusBarWindowIsAlwaysVisible() {
     assertWm { this.isAboveAppWindowVisible(ComponentNameMatcher.STATUS_BAR) }
 }
 
@@ -35,7 +35,7 @@
  * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows in all WM
  * trace entries
  */
-fun FlickerTestParameter.navBarWindowIsAlwaysVisible() {
+fun FlickerTest.navBarWindowIsAlwaysVisible() {
     assertWm { this.isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
@@ -43,7 +43,7 @@
  * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
  * start and end of the WM trace
  */
-fun FlickerTestParameter.navBarWindowIsVisibleAtStartAndEnd() {
+fun FlickerTest.navBarWindowIsVisibleAtStartAndEnd() {
     this.navBarWindowIsVisibleAtStart()
     this.navBarWindowIsVisibleAtEnd()
 }
@@ -52,7 +52,7 @@
  * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
  * start of the WM trace
  */
-fun FlickerTestParameter.navBarWindowIsVisibleAtStart() {
+fun FlickerTest.navBarWindowIsVisibleAtStart() {
     assertWmStart { this.isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
@@ -60,7 +60,7 @@
  * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the end
  * of the WM trace
  */
-fun FlickerTestParameter.navBarWindowIsVisibleAtEnd() {
+fun FlickerTest.navBarWindowIsVisibleAtEnd() {
     assertWmEnd { this.isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
@@ -68,7 +68,7 @@
  * Checks that [ComponentNameMatcher.TASK_BAR] window is visible and above the app windows in all WM
  * trace entries
  */
-fun FlickerTestParameter.taskBarWindowIsAlwaysVisible() {
+fun FlickerTest.taskBarWindowIsAlwaysVisible() {
     assertWm { this.isAboveAppWindowVisible(ComponentNameMatcher.TASK_BAR) }
 }
 
@@ -76,7 +76,7 @@
  * Checks that [ComponentNameMatcher.TASK_BAR] window is visible and above the app windows in all WM
  * trace entries
  */
-fun FlickerTestParameter.taskBarWindowIsVisibleAtEnd() {
+fun FlickerTest.taskBarWindowIsVisibleAtEnd() {
     assertWmEnd { this.isAboveAppWindowVisible(ComponentNameMatcher.TASK_BAR) }
 }
 
@@ -90,7 +90,7 @@
  * @param allStates if all states should be checked, othersie, just initial and final
  */
 @JvmOverloads
-fun FlickerTestParameter.entireScreenCovered(allStates: Boolean = true) {
+fun FlickerTest.entireScreenCovered(allStates: Boolean = true) {
     if (allStates) {
         assertLayers {
             this.invoke("entireScreenCovered") { entry ->
@@ -114,19 +114,19 @@
 }
 
 /** Checks that [ComponentNameMatcher.NAV_BAR] layer is visible at the start of the SF trace */
-fun FlickerTestParameter.navBarLayerIsVisibleAtStart() {
+fun FlickerTest.navBarLayerIsVisibleAtStart() {
     assertLayersStart { this.isVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
 /** Checks that [ComponentNameMatcher.NAV_BAR] layer is visible at the end of the SF trace */
-fun FlickerTestParameter.navBarLayerIsVisibleAtEnd() {
+fun FlickerTest.navBarLayerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
 /**
  * Checks that [ComponentNameMatcher.NAV_BAR] layer is visible at the start and end of the SF trace
  */
-fun FlickerTestParameter.navBarLayerIsVisibleAtStartAndEnd() {
+fun FlickerTest.navBarLayerIsVisibleAtStartAndEnd() {
     this.navBarLayerIsVisibleAtStart()
     this.navBarLayerIsVisibleAtEnd()
 }
@@ -134,18 +134,18 @@
 /**
  * Checks that [ComponentNameMatcher.TASK_BAR] layer is visible at the start and end of the SF trace
  */
-fun FlickerTestParameter.taskBarLayerIsVisibleAtStartAndEnd() {
+fun FlickerTest.taskBarLayerIsVisibleAtStartAndEnd() {
     this.taskBarLayerIsVisibleAtStart()
     this.taskBarLayerIsVisibleAtEnd()
 }
 
 /** Checks that [ComponentNameMatcher.TASK_BAR] layer is visible at the start of the SF trace */
-fun FlickerTestParameter.taskBarLayerIsVisibleAtStart() {
+fun FlickerTest.taskBarLayerIsVisibleAtStart() {
     assertLayersStart { this.isVisible(ComponentNameMatcher.TASK_BAR) }
 }
 
 /** Checks that [ComponentNameMatcher.TASK_BAR] layer is visible at the end of the SF trace */
-fun FlickerTestParameter.taskBarLayerIsVisibleAtEnd() {
+fun FlickerTest.taskBarLayerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
 }
 
@@ -153,7 +153,7 @@
  * Checks that [ComponentNameMatcher.STATUS_BAR] layer is visible at the start and end of the SF
  * trace
  */
-fun FlickerTestParameter.statusBarLayerIsVisibleAtStartAndEnd() {
+fun FlickerTest.statusBarLayerIsVisibleAtStartAndEnd() {
     assertLayersStart { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
     assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
 }
@@ -162,12 +162,14 @@
  * Asserts that the [ComponentNameMatcher.NAV_BAR] layer is at the correct position at the start of
  * the SF trace
  */
-fun FlickerTestParameter.navBarLayerPositionAtStart() {
+fun FlickerTest.navBarLayerPositionAtStart() {
     assertLayersStart {
         val display =
             this.entry.displays.firstOrNull { !it.isVirtual } ?: error("There is no display!")
         this.visibleRegion(ComponentNameMatcher.NAV_BAR)
-            .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation))
+            .coversExactly(
+                WindowUtils.getNavigationBarPosition(display, scenario.isGesturalNavigation)
+            )
     }
 }
 
@@ -175,13 +177,15 @@
  * Asserts that the [ComponentNameMatcher.NAV_BAR] layer is at the correct position at the end of
  * the SF trace
  */
-fun FlickerTestParameter.navBarLayerPositionAtEnd() {
+fun FlickerTest.navBarLayerPositionAtEnd() {
     assertLayersEnd {
         val display =
             this.entry.displays.minByOrNull { it.id }
                 ?: throw RuntimeException("There is no display!")
         this.visibleRegion(ComponentNameMatcher.NAV_BAR)
-            .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation))
+            .coversExactly(
+                WindowUtils.getNavigationBarPosition(display, scenario.isGesturalNavigation)
+            )
     }
 }
 
@@ -189,7 +193,7 @@
  * Asserts that the [ComponentNameMatcher.NAV_BAR] layer is at the correct position at the start and
  * end of the SF trace
  */
-fun FlickerTestParameter.navBarLayerPositionAtStartAndEnd() {
+fun FlickerTest.navBarLayerPositionAtStartAndEnd() {
     navBarLayerPositionAtStart()
     navBarLayerPositionAtEnd()
 }
@@ -198,7 +202,7 @@
  * Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the start
  * of the SF trace
  */
-fun FlickerTestParameter.statusBarLayerPositionAtStart() {
+fun FlickerTest.statusBarLayerPositionAtStart() {
     assertLayersStart {
         val display =
             this.entry.displays.minByOrNull { it.id }
@@ -212,7 +216,7 @@
  * Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the end of
  * the SF trace
  */
-fun FlickerTestParameter.statusBarLayerPositionAtEnd() {
+fun FlickerTest.statusBarLayerPositionAtEnd() {
     assertLayersEnd {
         val display =
             this.entry.displays.minByOrNull { it.id }
@@ -226,7 +230,7 @@
  * Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the start
  * and end of the SF trace
  */
-fun FlickerTestParameter.statusBarLayerPositionAtStartAndEnd() {
+fun FlickerTest.statusBarLayerPositionAtStartAndEnd() {
     statusBarLayerPositionAtStart()
     statusBarLayerPositionAtEnd()
 }
@@ -235,9 +239,7 @@
  * Asserts that the visibleRegion of the [ComponentNameMatcher.SNAPSHOT] layer can cover the
  * visibleRegion of the given app component exactly
  */
-fun FlickerTestParameter.snapshotStartingWindowLayerCoversExactlyOnApp(
-    component: IComponentNameMatcher
-) {
+fun FlickerTest.snapshotStartingWindowLayerCoversExactlyOnApp(component: IComponentNameMatcher) {
     assertLayers {
         invoke("snapshotStartingWindowLayerCoversExactlyOnApp") {
             val snapshotLayers =
@@ -291,7 +293,7 @@
  *      otherwise we won't and the layer must appear immediately.
  * ```
  */
-fun FlickerTestParameter.replacesLayer(
+fun FlickerTest.replacesLayer(
     originalLayer: IComponentNameMatcher,
     newLayer: IComponentNameMatcher,
     ignoreEntriesWithRotationLayer: Boolean = false,
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
index 4cf6691..b7bdeeb7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
@@ -17,11 +17,11 @@
 package com.android.server.wm.flicker.activityembedding
 
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
 import org.junit.Before
 
-abstract class ActivityEmbeddingTestBase(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+abstract class ActivityEmbeddingTestBase(flicker: FlickerTest) : BaseTest(flicker) {
     val testApp = ActivityEmbeddingAppHelper(instrumentation)
 
     @Before
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt
index b23fb5a..ea67729 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt
@@ -17,14 +17,12 @@
 package com.android.server.wm.flicker.activityembedding
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,8 +39,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenActivityEmbeddingPlaceholderSplit(testSpec: FlickerTestParameter) :
-    ActivityEmbeddingTestBase(testSpec) {
+class OpenActivityEmbeddingPlaceholderSplit(flicker: FlickerTest) :
+    ActivityEmbeddingTestBase(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -60,7 +58,7 @@
     @Presubmit
     @Test
     fun mainActivityBecomesInvisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
                 .then()
                 .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
@@ -70,12 +68,12 @@
     @Presubmit
     @Test
     fun placeholderSplitBecomesVisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
                 .then()
                 .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
         }
-        testSpec.assertLayers {
+        flicker.assertLayers {
             isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
                 .then()
                 .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
@@ -142,21 +140,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90),
-                    supportedNavigationModes =
-                        listOf(
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                        )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
index b16bfe0..d891714 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
@@ -18,11 +18,11 @@
 
 import android.platform.test.annotations.FlakyTest
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.annotation.FlickerServiceCompatible
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -69,7 +69,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseAppBackButtonTest(testSpec: FlickerTestParameter) : CloseAppTransition(testSpec) {
+class CloseAppBackButtonTest(flicker: FlickerTest) : CloseAppTransition(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -89,13 +89,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): List<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
index 78d0860..cc8ef1d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
@@ -18,11 +18,11 @@
 
 import android.platform.test.annotations.FlakyTest
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.annotation.FlickerServiceCompatible
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -69,7 +69,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseAppHomeButtonTest(testSpec: FlickerTestParameter) : CloseAppTransition(testSpec) {
+class CloseAppHomeButtonTest(flicker: FlickerTest) : CloseAppTransition(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -91,16 +91,11 @@
     override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
-         */
+        /** Creates the test configurations. */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
index 5bb227f..23503d2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
@@ -18,8 +18,8 @@
 
 import android.platform.test.annotations.Presubmit
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.StandardAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
@@ -28,15 +28,15 @@
 import org.junit.Test
 
 /** Base test class for transitions that close an app back to the launcher screen */
-abstract class CloseAppTransition(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+abstract class CloseAppTransition(flicker: FlickerTest) : BaseTest(flicker) {
     protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
-            tapl.setExpectedRotation(testSpec.startRotation)
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
             testApp.launchViaIntent(wmHelper)
-            this.setRotation(testSpec.startRotation)
+            this.setRotation(flicker.scenario.startRotation)
         }
         teardown { testApp.exit(wmHelper) }
     }
@@ -48,7 +48,7 @@
     @Presubmit
     @Test
     open fun launcherReplacesAppWindowAsTopWindow() {
-        testSpec.assertWm { this.isAppWindowOnTop(testApp).then().isAppWindowOnTop(LAUNCHER) }
+        flicker.assertWm { this.isAppWindowOnTop(testApp).then().isAppWindowOnTop(LAUNCHER) }
     }
 
     /**
@@ -58,17 +58,17 @@
     @Presubmit
     @Test
     open fun launcherWindowBecomesVisible() {
-        testSpec.assertWm { this.isAppWindowNotOnTop(LAUNCHER).then().isAppWindowOnTop(LAUNCHER) }
+        flicker.assertWm { this.isAppWindowNotOnTop(LAUNCHER).then().isAppWindowOnTop(LAUNCHER) }
     }
 
     /** Checks that [LAUNCHER] layer becomes visible when [testApp] becomes invisible */
     @Presubmit
     @Test
     open fun launcherLayerReplacesApp() {
-        testSpec.replacesLayer(
+        flicker.replacesLayer(
             testApp,
             LAUNCHER,
-            ignoreEntriesWithRotationLayer = testSpec.isLandscapeOrSeascapeAtStart
+            ignoreEntriesWithRotationLayer = flicker.scenario.isLandscapeOrSeascapeAtStart
         )
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index 48e1e64..368cc56 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import android.util.Log
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
@@ -37,10 +35,8 @@
 constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.ActivityEmbedding.MainActivity.LABEL,
-    component: ComponentNameMatcher = MAIN_ACTIVITY_COMPONENT,
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+    component: ComponentNameMatcher = MAIN_ACTIVITY_COMPONENT
+) : StandardAppHelper(instr, launcherName, component) {
 
     /**
      * Clicks the button to launch the placeholder primary activity, which should launch the
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt
index efb92f2..18563ff 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt
@@ -25,45 +25,52 @@
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import org.junit.Assert.assertNotNull
 
-class AssistantAppHelper @JvmOverloads constructor(
+class AssistantAppHelper
+@JvmOverloads
+constructor(
     val instr: Instrumentation,
     val component: ComponentName = ActivityOptions.ASSISTANT_SERVICE_COMPONENT_NAME,
 ) {
     protected val uiDevice: UiDevice = UiDevice.getInstance(instr)
-    protected val defaultAssistant: String? = Settings.Secure.getString(
-        instr.targetContext.contentResolver,
-        Settings.Secure.ASSISTANT)
-    protected val defaultVoiceInteractionService: String? = Settings.Secure.getString(
-        instr.targetContext.contentResolver,
-        Settings.Secure.VOICE_INTERACTION_SERVICE)
+    protected val defaultAssistant: String? =
+        Settings.Secure.getString(instr.targetContext.contentResolver, Settings.Secure.ASSISTANT)
+    protected val defaultVoiceInteractionService: String? =
+        Settings.Secure.getString(
+            instr.targetContext.contentResolver,
+            Settings.Secure.VOICE_INTERACTION_SERVICE
+        )
 
     fun setDefaultAssistant() {
         Settings.Secure.putString(
             instr.targetContext.contentResolver,
             Settings.Secure.VOICE_INTERACTION_SERVICE,
-            component.flattenToString())
+            component.flattenToString()
+        )
         Settings.Secure.putString(
             instr.targetContext.contentResolver,
             Settings.Secure.ASSISTANT,
-            component.flattenToString())
+            component.flattenToString()
+        )
     }
 
     fun resetDefaultAssistant() {
         Settings.Secure.putString(
             instr.targetContext.contentResolver,
             Settings.Secure.VOICE_INTERACTION_SERVICE,
-            defaultVoiceInteractionService)
+            defaultVoiceInteractionService
+        )
         Settings.Secure.putString(
             instr.targetContext.contentResolver,
             Settings.Secure.ASSISTANT,
-            defaultAssistant)
+            defaultAssistant
+        )
     }
 
     /**
      * Open Assistance UI.
      *
-     * @param longpress open the UI by long pressing power button.
-     *  Otherwise open the UI through vioceinteraction shell command directly.
+     * @param longpress open the UI by long pressing power button. Otherwise open the UI through
+     * vioceinteraction shell command directly.
      */
     @JvmOverloads
     fun openUI(longpress: Boolean = false) {
@@ -72,9 +79,11 @@
         } else {
             uiDevice.executeShellCommand("cmd voiceinteraction show")
         }
-        val ui = uiDevice.wait(
-            Until.findObject(By.res(ActivityOptions.FLICKER_APP_PACKAGE, "vis_frame")),
-            FIND_TIMEOUT)
+        val ui =
+            uiDevice.wait(
+                Until.findObject(By.res(ActivityOptions.FLICKER_APP_PACKAGE, "vis_frame")),
+                FIND_TIMEOUT
+            )
         assertNotNull("Can't find Assistant UI after long pressing power button.", ui)
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt
index 4340bd7..05b50f0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FixedOrientationAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
@@ -29,7 +27,5 @@
     instr: Instrumentation,
     launcherName: String = ActivityOptions.PortraitOnlyActivity.LABEL,
     component: ComponentNameMatcher =
-        ActivityOptions.PortraitOnlyActivity.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy)
+        ActivityOptions.PortraitOnlyActivity.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
index 73cb862..2ae8e1d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
@@ -18,15 +18,16 @@
 
 package com.android.server.wm.flicker.helpers
 
-import com.android.server.wm.flicker.Flicker
+import com.android.server.wm.flicker.IFlickerTestData
 import com.android.server.wm.flicker.rules.ChangeDisplayOrientationRule
+import com.android.server.wm.traces.common.service.PlatformConsts
 
 /**
  * Changes the device [rotation] and wait for the rotation animation to complete
  *
  * @param rotation New device rotation
  */
-fun Flicker.setRotation(rotation: Int) =
+fun IFlickerTestData.setRotation(rotation: PlatformConsts.Rotation) =
     ChangeDisplayOrientationRule.setRotation(
         rotation,
         instrumentation,
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
index d45315e..d583bba 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Direction
 import androidx.test.uiautomator.Until
@@ -32,10 +30,8 @@
 constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.Game.LABEL,
-    component: ComponentNameMatcher = ActivityOptions.Game.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy,
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+    component: ComponentNameMatcher = ActivityOptions.Game.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component) {
 
     /**
      * Swipes down in the mock game app.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
index ca5b2af..b7eea1b 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
@@ -26,6 +26,7 @@
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.Condition
 import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import java.util.regex.Pattern
@@ -34,7 +35,7 @@
 @JvmOverloads
 constructor(
     instr: Instrumentation,
-    private val rotation: Int,
+    private val rotation: PlatformConsts.Rotation,
     private val imePackageName: String = IME_PACKAGE,
     launcherName: String = ActivityOptions.Ime.AutoFocusActivity.LABEL,
     component: ComponentNameMatcher =
@@ -63,7 +64,7 @@
             } else {
                 getPackage()
             }
-        launcherStrategy.launch(appName, expectedPackage)
+        open(expectedPackage)
     }
 
     fun startDialogThemedActivity(wmHelper: WindowManagerStateHelper) {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
index cefbf18..3bb7f4e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
 import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -31,10 +29,8 @@
 constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.Ime.Default.LABEL,
-    component: ComponentNameMatcher = ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+    component: ComponentNameMatcher = ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component) {
     /**
      * Opens the IME and wait for it to be displayed
      *
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt
index 1502ad5..69d6a47 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeStateInitializeHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
@@ -29,7 +27,5 @@
     instr: Instrumentation,
     launcherName: String = ActivityOptions.Ime.StateInitializeActivity.LABEL,
     component: ComponentNameMatcher =
-        ActivityOptions.Ime.StateInitializeActivity.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy)
+        ActivityOptions.Ime.StateInitializeActivity.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt
index f00904b..d0935ef 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Direction
 import androidx.test.uiautomator.UiObject2
@@ -32,10 +30,8 @@
 constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.Mail.LABEL,
-    component: ComponentNameMatcher = ActivityOptions.Mail.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+    component: ComponentNameMatcher = ActivityOptions.Mail.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component) {
 
     fun openMail(rowIdx: Int) {
         val rowSel =
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt
index 34294a6..8b3fa18 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
@@ -32,10 +30,8 @@
 constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.LaunchNewTask.LABEL,
-    component: ComponentNameMatcher = ActivityOptions.LaunchNewTask.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+    component: ComponentNameMatcher = ActivityOptions.LaunchNewTask.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component) {
     fun openNewTask(device: UiDevice, wmHelper: WindowManagerStateHelper) {
         val button =
             device.wait(Until.findObject(By.res(getPackage(), "launch_new_task")), FIND_TIMEOUT)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt
index bbb782d..992a1a1 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NonResizeableAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
@@ -29,7 +27,5 @@
     instr: Instrumentation,
     launcherName: String = ActivityOptions.NonResizeableActivity.LABEL,
     component: ComponentNameMatcher =
-        ActivityOptions.NonResizeableActivity.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy)
+        ActivityOptions.NonResizeableActivity.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt
index a3e32e5..c29c752 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
 import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -31,10 +29,8 @@
 constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.Notification.LABEL,
-    component: ComponentNameMatcher = ActivityOptions.Notification.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+    component: ComponentNameMatcher = ActivityOptions.Notification.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component) {
     fun postNotification(wmHelper: WindowManagerStateHelper) {
         val button =
             uiDevice.wait(Until.findObject(By.res(getPackage(), "post_notification")), FIND_TIMEOUT)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index 19ee09a..8fe6aac 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -92,8 +92,12 @@
 
         // if the distance per step is less than 1, carry out the animation in two steps
         gestureHelper.pinch(
-                Tuple(initLeftX, yCoord), Tuple(initRightX, yCoord),
-                Tuple(finalLeftX, yCoord), Tuple(finalRightX, yCoord), adjustedSteps)
+            Tuple(initLeftX, yCoord),
+            Tuple(initRightX, yCoord),
+            Tuple(finalLeftX, yCoord),
+            Tuple(finalRightX, yCoord),
+            adjustedSteps
+        )
 
         waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect))
     }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
index c904352..c51754c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SeamlessRotationAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
@@ -29,7 +27,5 @@
     instr: Instrumentation,
     launcherName: String = ActivityOptions.SeamlessRotation.LABEL,
     component: ComponentNameMatcher =
-        ActivityOptions.SeamlessRotation.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy)
+        ActivityOptions.SeamlessRotation.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt
index de152cb5..9318f20 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ShowWhenLockedAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
@@ -29,7 +27,5 @@
     instr: Instrumentation,
     launcherName: String = ActivityOptions.ShowWhenLockedActivity.LABEL,
     component: ComponentNameMatcher =
-        ActivityOptions.ShowWhenLockedActivity.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy)
+        ActivityOptions.ShowWhenLockedActivity.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
index e415990..b46ff2c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/SimpleAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
@@ -28,7 +26,5 @@
 constructor(
     instr: Instrumentation,
     launcherName: String = ActivityOptions.SimpleActivity.LABEL,
-    component: ComponentNameMatcher = ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy)
+    component: ComponentNameMatcher = ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt
index 1330190..720d962 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt
@@ -17,8 +17,6 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
-import android.support.test.launcherhelper.ILauncherStrategy
-import android.support.test.launcherhelper.LauncherStrategyFactory
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
@@ -33,10 +31,8 @@
     instr: Instrumentation,
     launcherName: String = ActivityOptions.LaunchNewActivity.LABEL,
     component: ComponentNameMatcher =
-        ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent(),
-    launcherStrategy: ILauncherStrategy =
-        LauncherStrategyFactory.getInstance(instr).launcherStrategy
-) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+        ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent()
+) : StandardAppHelper(instr, launcherName, component) {
 
     private val secondActivityComponent =
         ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
index 1a49595..c735be0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
@@ -17,16 +17,15 @@
 package com.android.server.wm.flicker.ime
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,8 +48,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeAutoOpenWindowToAppTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
-    private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+class CloseImeAutoOpenWindowToAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+    private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -62,43 +61,37 @@
     @Presubmit
     @Test
     fun imeAppWindowIsAlwaysVisible() {
-        testSpec.assertWm { this.isAppWindowOnTop(testApp) }
+        flicker.assertWm { this.isAppWindowOnTop(testApp) }
     }
 
     @Presubmit
     @Test
     fun imeLayerVisibleStart() {
-        testSpec.assertLayersStart { this.isVisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersStart { this.isVisible(ComponentNameMatcher.IME) }
     }
 
     @Presubmit
     @Test
     fun imeLayerInvisibleEnd() {
-        testSpec.assertLayersEnd { this.isInvisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersEnd { this.isInvisible(ComponentNameMatcher.IME) }
     }
 
-    @Presubmit @Test fun imeLayerBecomesInvisible() = testSpec.imeLayerBecomesInvisible()
+    @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
 
     @Presubmit
     @Test
     fun imeAppLayerIsAlwaysVisible() {
-        testSpec.assertLayers { this.isVisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp) }
     }
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    // b/190352379 (IME doesn't show on app launch in 90 degrees)
-                    supportedRotations = listOf(Surface.ROTATION_0),
-                    supportedNavigationModes =
-                        listOf(
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                        )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                // b/190352379 (IME doesn't show on app launch in 90 degrees)
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
index 463efe8..4024f56 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
@@ -17,16 +17,15 @@
 package com.android.server.wm.flicker.ime
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,8 +48,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeAutoOpenWindowToHomeTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
-    private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+class CloseImeAutoOpenWindowToHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
+    private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -68,43 +67,37 @@
     @Presubmit
     @Test
     fun imeAppWindowBecomesInvisible() {
-        testSpec.assertWm { this.isAppWindowOnTop(testApp).then().isAppWindowNotOnTop(testApp) }
+        flicker.assertWm { this.isAppWindowOnTop(testApp).then().isAppWindowNotOnTop(testApp) }
     }
 
     @Presubmit
     @Test
     fun imeLayerVisibleStart() {
-        testSpec.assertLayersStart { this.isVisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersStart { this.isVisible(ComponentNameMatcher.IME) }
     }
 
     @Presubmit
     @Test
     fun imeLayerInvisibleEnd() {
-        testSpec.assertLayersEnd { this.isInvisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersEnd { this.isInvisible(ComponentNameMatcher.IME) }
     }
 
-    @Presubmit @Test fun imeLayerBecomesInvisible() = testSpec.imeLayerBecomesInvisible()
+    @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
 
     @Presubmit
     @Test
     fun imeAppLayerBecomesInvisible() {
-        testSpec.assertLayers { this.isVisible(testApp).then().isInvisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp).then().isInvisible(testApp) }
     }
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    // b/190352379 (IME doesn't show on app launch in 90 degrees)
-                    supportedRotations = listOf(Surface.ROTATION_0),
-                    supportedNavigationModes =
-                        listOf(
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                        )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                // b/190352379 (IME doesn't show on app launch in 90 degrees)
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt
index 91d9a1f..c72405c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeEditorPopupDialogTest.kt
@@ -17,17 +17,16 @@
 package com.android.server.wm.flicker.ime
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeEditorPopupDialogAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.traces.region.RegionSubject
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -38,7 +37,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeEditorPopupDialogTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+class CloseImeEditorPopupDialogTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val imeTestApp = ImeEditorPopupDialogAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -59,14 +58,12 @@
         }
     }
 
-    @Presubmit
-    @Test
-    fun imeWindowBecameInvisible() = testSpec.imeWindowBecomesInvisible()
+    @Presubmit @Test fun imeWindowBecameInvisible() = flicker.imeWindowBecomesInvisible()
 
     @Presubmit
     @Test
     fun imeLayerAndImeSnapshotVisibleOnScreen() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(ComponentNameMatcher.IME)
                 .then()
                 .isVisible(ComponentNameMatcher.IME_SNAPSHOT)
@@ -79,7 +76,7 @@
     @Presubmit
     @Test
     fun imeSnapshotAssociatedOnAppVisibleRegion() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.invoke("imeSnapshotAssociatedOnAppVisibleRegion") {
                 val imeSnapshotLayers =
                     it.subjects.filter { subject ->
@@ -106,16 +103,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedNavigationModes =
-                    listOf(
-                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                        WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                    ),
-                    supportedRotations = listOf(Surface.ROTATION_0)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
index ef42766..afc5f65 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
@@ -21,11 +21,11 @@
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.Assume
@@ -43,7 +43,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToAppTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+class CloseImeWindowToAppTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -60,7 +60,7 @@
     @Presubmit
     @Test
     override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.visibleWindowsShownMoreThanOneConsecutiveEntry(
                 listOf(
                     ComponentNameMatcher.IME,
@@ -75,31 +75,31 @@
     @Presubmit
     @Test
     override fun navBarLayerPositionAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        Assume.assumeFalse(testSpec.isLandscapeOrSeascapeAtStart)
-        testSpec.navBarLayerPositionAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        Assume.assumeFalse(flicker.scenario.isLandscapeOrSeascapeAtStart)
+        flicker.navBarLayerPositionAtStartAndEnd()
     }
 
     @FlakyTest
     @Test
     fun navBarLayerPositionAtStartAndEndLandscapeOrSeascapeAtStart() {
-        Assume.assumeFalse(testSpec.isTablet)
-        Assume.assumeTrue(testSpec.isLandscapeOrSeascapeAtStart)
-        testSpec.navBarLayerPositionAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart)
+        flicker.navBarLayerPositionAtStartAndEnd()
     }
 
-    @Presubmit @Test fun imeLayerBecomesInvisible() = testSpec.imeLayerBecomesInvisible()
+    @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
 
     @Presubmit
     @Test
     fun imeAppLayerIsAlwaysVisible() {
-        testSpec.assertLayers { this.isVisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp) }
     }
 
     @Presubmit
     @Test
     fun imeAppWindowIsAlwaysVisible() {
-        testSpec.assertWm { this.isAppWindowOnTop(testApp) }
+        flicker.assertWm { this.isAppWindowOnTop(testApp) }
     }
 
     @Test
@@ -115,8 +115,8 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
index c92fce3..aedf965 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
@@ -18,16 +18,15 @@
 
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -42,7 +41,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+class CloseImeWindowToHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -63,7 +62,7 @@
     @Presubmit
     @Test
     override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.visibleWindowsShownMoreThanOneConsecutiveEntry(
                 listOf(
                     ComponentNameMatcher.IME,
@@ -78,27 +77,27 @@
     @Presubmit
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.visibleLayersShownMoreThanOneConsecutiveEntry(
                 listOf(ComponentNameMatcher.IME, ComponentNameMatcher.SPLASH_SCREEN)
             )
         }
     }
 
-    @Presubmit @Test fun imeLayerBecomesInvisible() = testSpec.imeLayerBecomesInvisible()
+    @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
 
-    @Presubmit @Test fun imeWindowBecomesInvisible() = testSpec.imeWindowBecomesInvisible()
+    @Presubmit @Test fun imeWindowBecomesInvisible() = flicker.imeWindowBecomesInvisible()
 
     @Presubmit
     @Test
     fun imeAppWindowBecomesInvisible() {
-        testSpec.assertWm { this.isAppWindowVisible(testApp).then().isAppWindowInvisible(testApp) }
+        flicker.assertWm { this.isAppWindowVisible(testApp).then().isAppWindowInvisible(testApp) }
     }
 
     @Presubmit
     @Test
     fun imeAppLayerBecomesInvisible() {
-        testSpec.assertLayers { this.isVisible(testApp).then().isInvisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp).then().isInvisible(testApp) }
     }
 
     @Test
@@ -115,16 +114,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0),
-                    supportedNavigationModes =
-                        listOf(
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                        )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
index e0c5edc..3edc15f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
@@ -18,22 +18,22 @@
 
 package com.android.server.wm.flicker.ime
 
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.traces.common.ComponentNameMatcher
 
-fun FlickerTestParameter.imeLayerBecomesVisible() {
+fun FlickerTest.imeLayerBecomesVisible() {
     assertLayers {
         this.isInvisible(ComponentNameMatcher.IME).then().isVisible(ComponentNameMatcher.IME)
     }
 }
 
-fun FlickerTestParameter.imeLayerBecomesInvisible() {
+fun FlickerTest.imeLayerBecomesInvisible() {
     assertLayers {
         this.isVisible(ComponentNameMatcher.IME).then().isInvisible(ComponentNameMatcher.IME)
     }
 }
 
-fun FlickerTestParameter.imeWindowIsAlwaysVisible(rotatesScreen: Boolean = false) {
+fun FlickerTest.imeWindowIsAlwaysVisible(rotatesScreen: Boolean = false) {
     if (rotatesScreen) {
         assertWm {
             this.isNonAppWindowVisible(ComponentNameMatcher.IME)
@@ -47,7 +47,7 @@
     }
 }
 
-fun FlickerTestParameter.imeWindowBecomesVisible() {
+fun FlickerTest.imeWindowBecomesVisible() {
     assertWm {
         this.isNonAppWindowInvisible(ComponentNameMatcher.IME)
             .then()
@@ -55,7 +55,7 @@
     }
 }
 
-fun FlickerTestParameter.imeWindowBecomesInvisible() {
+fun FlickerTest.imeWindowBecomesInvisible() {
     assertWm {
         this.isNonAppWindowVisible(ComponentNameMatcher.IME)
             .then()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt
index 073da4f..da3c62d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeAndDialogThemeAppTest.kt
@@ -18,19 +18,18 @@
 
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import android.view.WindowInsets.Type.ime
 import android.view.WindowInsets.Type.navigationBars
 import android.view.WindowInsets.Type.statusBars
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
@@ -47,8 +46,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class LaunchAppShowImeAndDialogThemeAppTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
-    private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+class LaunchAppShowImeAndDialogThemeAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+    private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -75,42 +74,36 @@
     @Test
     override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
-    /** Checks that [ComponentMatcher.IME] layer becomes visible during the transition */
-    @Presubmit @Test fun imeWindowIsAlwaysVisible() = testSpec.imeWindowIsAlwaysVisible()
+    /** Checks that [ComponentNameMatcher.IME] layer becomes visible during the transition */
+    @Presubmit @Test fun imeWindowIsAlwaysVisible() = flicker.imeWindowIsAlwaysVisible()
 
-    /** Checks that [ComponentMatcher.IME] layer is visible at the end of the transition */
+    /** Checks that [ComponentNameMatcher.IME] layer is visible at the end of the transition */
     @Presubmit
     @Test
     fun imeLayerExistsEnd() {
-        testSpec.assertLayersEnd { this.isVisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.IME) }
     }
 
-    /** Checks that [ComponentMatcher.IME_SNAPSHOT] layer is invisible always. */
+    /** Checks that [ComponentNameMatcher.IME_SNAPSHOT] layer is invisible always. */
     @Presubmit
     @Test
     fun imeSnapshotNotVisible() {
-        testSpec.assertLayers { this.isInvisible(ComponentNameMatcher.IME_SNAPSHOT) }
+        flicker.assertLayers { this.isInvisible(ComponentNameMatcher.IME_SNAPSHOT) }
     }
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0),
-                    supportedNavigationModes =
-                        listOf(
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                        )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
index a93f176..4891901 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
@@ -17,18 +17,17 @@
 package com.android.server.wm.flicker.ime
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
 import com.android.server.wm.flicker.helpers.ImeStateInitializeHelper
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -73,15 +72,15 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class LaunchAppShowImeOnStartTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
-    private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+class LaunchAppShowImeOnStartTest(flicker: FlickerTest) : BaseTest(flicker) {
+    private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
     private val initializeApp = ImeStateInitializeHelper(instrumentation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
             initializeApp.launchViaIntent(wmHelper)
-            this.setRotation(testSpec.startRotation)
+            this.setRotation(flicker.scenario.startRotation)
         }
         teardown {
             initializeApp.exit(wmHelper)
@@ -93,45 +92,39 @@
         }
     }
 
-    /** Checks that [ComponentMatcher.IME] window becomes visible during the transition */
-    @Presubmit @Test fun imeWindowBecomesVisible() = testSpec.imeWindowBecomesVisible()
+    /** Checks that [ComponentNameMatcher.IME] window becomes visible during the transition */
+    @Presubmit @Test fun imeWindowBecomesVisible() = flicker.imeWindowBecomesVisible()
 
-    /** Checks that [ComponentMatcher.IME] layer becomes visible during the transition */
-    @Presubmit @Test fun imeLayerBecomesVisible() = testSpec.imeLayerBecomesVisible()
+    /** Checks that [ComponentNameMatcher.IME] layer becomes visible during the transition */
+    @Presubmit @Test fun imeLayerBecomesVisible() = flicker.imeLayerBecomesVisible()
 
-    /** Checks that [ComponentMatcher.IME] layer is invisible at the start of the transition */
+    /** Checks that [ComponentNameMatcher.IME] layer is invisible at the start of the transition */
     @Presubmit
     @Test
     fun imeLayerNotExistsStart() {
-        testSpec.assertLayersStart { this.isInvisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.IME) }
     }
 
-    /** Checks that [ComponentMatcher.IME] layer is visible at the end of the transition */
+    /** Checks that [ComponentNameMatcher.IME] layer is visible at the end of the transition */
     @Presubmit
     @Test
     fun imeLayerExistsEnd() {
-        testSpec.assertLayersEnd { this.isVisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.IME) }
     }
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0),
-                    supportedNavigationModes =
-                        listOf(
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                        )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt
index 7d7953b..33e9574 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowAndCloseTest.kt
@@ -19,17 +19,16 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -47,7 +46,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowAndCloseTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+class OpenImeWindowAndCloseTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val simpleApp = SimpleAppHelper(instrumentation)
     private val testApp = ImeAppHelper(instrumentation)
 
@@ -62,9 +61,9 @@
         teardown { simpleApp.exit(wmHelper) }
     }
 
-    @Presubmit @Test fun imeWindowBecomesInvisible() = testSpec.imeWindowBecomesInvisible()
+    @Presubmit @Test fun imeWindowBecomesInvisible() = flicker.imeWindowBecomesInvisible()
 
-    @Presubmit @Test fun imeLayerBecomesInvisible() = testSpec.imeLayerBecomesInvisible()
+    @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
 
     @Presubmit
     @Test
@@ -91,16 +90,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0),
-                    supportedNavigationModes =
-                        listOf(
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                        )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
index 3e18d59..197564a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
@@ -19,17 +19,16 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.snapshotStartingWindowLayerCoversExactlyOnApp
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -46,9 +45,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowFromFixedOrientationAppTest(testSpec: FlickerTestParameter) :
-    BaseTest(testSpec) {
-    private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+class OpenImeWindowFromFixedOrientationAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+    private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -63,61 +61,58 @@
             wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
         }
         transitions {
-            // Bring the exist IME activity to the front in landscape mode device rotation.
-            setRotation(Surface.ROTATION_90)
+            // Bring the existing IME activity to the front in landscape mode device rotation.
+            setRotation(PlatformConsts.Rotation.ROTATION_90)
             imeTestApp.launchViaIntent(wmHelper)
         }
         teardown { imeTestApp.exit(wmHelper) }
     }
 
     /** {@inheritDoc} */
-    @Presubmit
+    @Postsubmit
     @Test
     override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
+    @Postsubmit
+    @Test
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
     /** {@inheritDoc} */
     @Postsubmit
     @Test
     override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
-    @Presubmit
-    @Test
-    fun imeWindowBecomesVisible() = testSpec.imeWindowBecomesVisible()
+    @Presubmit @Test fun imeWindowBecomesVisible() = flicker.imeWindowBecomesVisible()
 
-    @Presubmit
-    @Test
-    fun imeLayerBecomesVisible() = testSpec.imeLayerBecomesVisible()
+    @Presubmit @Test fun imeLayerBecomesVisible() = flicker.imeLayerBecomesVisible()
 
     @Postsubmit
     @Test
     fun snapshotStartingWindowLayerCoversExactlyOnApp() {
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp)
+        flicker.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp)
     }
 
     @Presubmit
     @Test
     fun snapshotStartingWindowLayerCoversExactlyOnApp_ShellTransit() {
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp)
+        flicker.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp)
     }
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_90),
-                    supportedNavigationModes =
-                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_90)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
index 9919d87..c097511 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowTest.kt
@@ -18,15 +18,14 @@
 
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -38,7 +37,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+class OpenImeWindowTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -60,35 +59,29 @@
         layerAlwaysVisible()
     }
 
-    @Presubmit @Test fun imeWindowBecomesVisible() = testSpec.imeWindowBecomesVisible()
+    @Presubmit @Test fun imeWindowBecomesVisible() = flicker.imeWindowBecomesVisible()
 
     @Presubmit
     @Test
     fun appWindowAlwaysVisibleOnTop() {
-        testSpec.assertWm { this.isAppWindowOnTop(testApp) }
+        flicker.assertWm { this.isAppWindowOnTop(testApp) }
     }
 
-    @Presubmit @Test fun imeLayerBecomesVisible() = testSpec.imeLayerBecomesVisible()
+    @Presubmit @Test fun imeLayerBecomesVisible() = flicker.imeLayerBecomesVisible()
 
     @Presubmit
     @Test
     fun layerAlwaysVisible() {
-        testSpec.assertLayers { this.isVisible(testApp) }
+        flicker.assertLayers { this.isVisible(testApp) }
     }
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0),
-                    supportedNavigationModes =
-                        listOf(
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                        )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt
index 0a7701e..209eb0c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt
@@ -18,15 +18,13 @@
 
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd
 import com.android.server.wm.flicker.statusBarLayerIsVisibleAtStartAndEnd
 import com.android.server.wm.traces.common.ComponentNameMatcher
@@ -48,8 +46,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowToOverViewTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
-    private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+class OpenImeWindowToOverViewTest(flicker: FlickerTest) : BaseTest(flicker) {
+    private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -82,7 +80,7 @@
      */
     private fun waitNavStatusBarVisibility(stateSync: WindowManagerStateHelper.StateSyncBuilder) {
         when {
-            testSpec.isLandscapeOrSeascapeAtStart && !testSpec.isTablet ->
+            flicker.scenario.isLandscapeOrSeascapeAtStart && !flicker.scenario.isTablet ->
                 stateSync.add(WindowManagerConditionsFactory.isStatusBarVisible().negate())
             else -> stateSync.withNavOrTaskBarVisible().withStatusBarVisible()
         }
@@ -91,25 +89,25 @@
     @Presubmit
     @Test
     fun imeWindowIsAlwaysVisible() {
-        testSpec.imeWindowIsAlwaysVisible()
+        flicker.imeWindowIsAlwaysVisible()
     }
 
     @Presubmit
     @Test
     fun navBarLayerIsVisibleAtStartAndEnd3Button() {
-        Assume.assumeFalse(testSpec.isTablet)
-        Assume.assumeFalse(testSpec.isGesturalNavigation)
-        testSpec.navBarLayerIsVisibleAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+        flicker.navBarLayerIsVisibleAtStartAndEnd()
     }
 
     /** Bars are expected to be hidden while entering overview in landscape (b/227189877) */
     @Presubmit
     @Test
     fun navBarLayerIsVisibleAtStartAndEndGestural() {
-        Assume.assumeFalse(testSpec.isTablet)
-        Assume.assumeTrue(testSpec.isGesturalNavigation)
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.navBarLayerIsVisibleAtStartAndEnd()
+        flicker.navBarLayerIsVisibleAtStartAndEnd()
     }
 
     /**
@@ -119,12 +117,12 @@
     @Presubmit
     @Test
     fun navBarLayerIsInvisibleInLandscapeGestural() {
-        Assume.assumeFalse(testSpec.isTablet)
-        Assume.assumeTrue(testSpec.isLandscapeOrSeascapeAtStart)
-        Assume.assumeTrue(testSpec.isGesturalNavigation)
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart)
+        Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.assertLayersStart { this.isVisible(ComponentNameMatcher.NAV_BAR) }
-        testSpec.assertLayersEnd { this.isInvisible(ComponentNameMatcher.NAV_BAR) }
+        flicker.assertLayersStart { this.isVisible(ComponentNameMatcher.NAV_BAR) }
+        flicker.assertLayersEnd { this.isInvisible(ComponentNameMatcher.NAV_BAR) }
     }
 
     /**
@@ -134,11 +132,11 @@
     @Presubmit
     @Test
     fun statusBarLayerIsInvisibleInLandscapePhone() {
-        Assume.assumeTrue(testSpec.isLandscapeOrSeascapeAtStart)
-        Assume.assumeTrue(testSpec.isGesturalNavigation)
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.assertLayersStart { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
-        testSpec.assertLayersEnd { this.isInvisible(ComponentNameMatcher.STATUS_BAR) }
+        Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart)
+        Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.assertLayersStart { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
+        flicker.assertLayersEnd { this.isInvisible(ComponentNameMatcher.STATUS_BAR) }
     }
 
     /**
@@ -148,35 +146,31 @@
     @Presubmit
     @Test
     fun statusBarLayerIsInvisibleInLandscapeTablet() {
-        Assume.assumeTrue(testSpec.isLandscapeOrSeascapeAtStart)
-        Assume.assumeTrue(testSpec.isGesturalNavigation)
-        Assume.assumeTrue(testSpec.isTablet)
-        testSpec.statusBarLayerIsVisibleAtStartAndEnd()
+        Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart)
+        Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.statusBarLayerIsVisibleAtStartAndEnd()
     }
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Visibility changes depending on orientation and navigation mode")
-    override fun navBarLayerIsVisibleAtStartAndEnd() {
-    }
+    override fun navBarLayerIsVisibleAtStartAndEnd() {}
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Visibility changes depending on orientation and navigation mode")
-    override fun navBarLayerPositionAtStartAndEnd() {
-    }
+    override fun navBarLayerPositionAtStartAndEnd() {}
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Visibility changes depending on orientation and navigation mode")
-    override fun statusBarLayerPositionAtStartAndEnd() {
-    }
+    override fun statusBarLayerPositionAtStartAndEnd() {}
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Visibility changes depending on orientation and navigation mode")
-    override fun statusBarLayerIsVisibleAtStartAndEnd() {
-    }
+    override fun statusBarLayerIsVisibleAtStartAndEnd() {}
 
     @Presubmit
     @Test
@@ -185,38 +179,38 @@
     @Presubmit
     @Test
     fun statusBarLayerIsVisibleInPortrait() {
-        Assume.assumeFalse(testSpec.isLandscapeOrSeascapeAtStart)
-        testSpec.statusBarLayerIsVisibleAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isLandscapeOrSeascapeAtStart)
+        flicker.statusBarLayerIsVisibleAtStartAndEnd()
     }
 
     @Presubmit
     @Test
     fun statusBarLayerIsInvisibleInLandscapeShell() {
-        Assume.assumeTrue(testSpec.isLandscapeOrSeascapeAtStart)
-        Assume.assumeFalse(testSpec.isTablet)
+        Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart)
+        Assume.assumeFalse(flicker.scenario.isTablet)
         Assume.assumeTrue(isShellTransitionsEnabled)
-        testSpec.assertLayersStart { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
-        testSpec.assertLayersEnd { this.isInvisible(ComponentNameMatcher.STATUS_BAR) }
+        flicker.assertLayersStart { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
+        flicker.assertLayersEnd { this.isInvisible(ComponentNameMatcher.STATUS_BAR) }
     }
 
     @Presubmit
     @Test
     fun statusBarLayerIsVisibleInLandscapeLegacy() {
-        Assume.assumeTrue(testSpec.isLandscapeOrSeascapeAtStart)
-        Assume.assumeTrue(testSpec.isTablet)
+        Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart)
+        Assume.assumeTrue(flicker.scenario.isTablet)
         Assume.assumeFalse(isShellTransitionsEnabled)
-        testSpec.statusBarLayerIsVisibleAtStartAndEnd()
+        flicker.statusBarLayerIsVisibleAtStartAndEnd()
     }
 
     @Presubmit
     @Test
     fun imeLayerIsVisibleAndAssociatedWithAppWidow() {
-        testSpec.assertLayersStart {
+        flicker.assertLayersStart {
             isVisible(ComponentNameMatcher.IME)
                 .visibleRegion(ComponentNameMatcher.IME)
                 .coversAtMost(isVisible(imeTestApp).visibleRegion(imeTestApp).region)
         }
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.invoke("imeLayerIsVisibleAndAlignAppWidow") {
                 val imeVisibleRegion = it.visibleRegion(ComponentNameMatcher.IME)
                 val appVisibleRegion = it.visibleRegion(imeTestApp)
@@ -232,21 +226,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90),
-                    supportedNavigationModes =
-                    listOf(
-                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
-                        WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
-                    )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
index f810fbb..38791a2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ReOpenImeWindowTest.kt
@@ -17,17 +17,17 @@
 package com.android.server.wm.flicker.ime
 
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,8 +41,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ReOpenImeWindowTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
-    private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+open class ReOpenImeWindowTest(flicker: FlickerTest) : BaseTest(flicker) {
+    private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -50,7 +50,7 @@
             tapl.workspace.switchToOverview().dismissAllTasks()
             testApp.launchViaIntent(wmHelper)
             testApp.openIME(wmHelper)
-            this.setRotation(testSpec.startRotation)
+            this.setRotation(flicker.scenario.startRotation)
             device.pressRecentApps()
             wmHelper.StateSyncBuilder().withRecentsActivityVisible().waitForAndVerify()
         }
@@ -68,7 +68,7 @@
         // depends on how much of the animation transactions are sent to SF at once
         // sometimes this layer appears for 2-3 frames, sometimes for only 1
         val recentTaskComponent = ComponentNameMatcher("", "RecentTaskScreenshotSurface")
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.visibleLayersShownMoreThanOneConsecutiveEntry(
                 listOf(
                     ComponentNameMatcher.SPLASH_SCREEN,
@@ -84,14 +84,14 @@
     @Test
     override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
         val component = ComponentNameMatcher("", "RecentTaskScreenshotSurface")
-        testSpec.assertWm {
+        flicker.assertWm {
             this.visibleWindowsShownMoreThanOneConsecutiveEntry(
                 ignoreWindows =
-                listOf(
-                    ComponentNameMatcher.SPLASH_SCREEN,
-                    ComponentNameMatcher.SNAPSHOT,
-                    component
-                )
+                    listOf(
+                        ComponentNameMatcher.SPLASH_SCREEN,
+                        ComponentNameMatcher.SNAPSHOT,
+                        component
+                    )
             )
         }
     }
@@ -99,16 +99,14 @@
     @Presubmit
     @Test
     fun launcherWindowBecomesInvisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowVisible(ComponentNameMatcher.LAUNCHER)
                 .then()
                 .isAppWindowInvisible(ComponentNameMatcher.LAUNCHER)
         }
     }
 
-    @Presubmit
-    @Test
-    fun imeWindowIsAlwaysVisible() = testSpec.imeWindowIsAlwaysVisible()
+    @Presubmit @Test fun imeWindowIsAlwaysVisible() = flicker.imeWindowIsAlwaysVisible()
 
     @Presubmit
     @Test
@@ -117,19 +115,19 @@
         // and exiting overview. Since we log 1x per frame, sometimes the activity visibility
         // and the app visibility are updated together, sometimes not, thus ignore activity
         // check at the start
-        testSpec.assertWm { this.isAppWindowVisible(testApp) }
+        flicker.assertWm { this.isAppWindowVisible(testApp) }
     }
 
     @Presubmit
     @Test
     fun imeLayerBecomesVisible() {
-        testSpec.assertLayers { this.isVisible(ComponentNameMatcher.IME) }
+        flicker.assertLayers { this.isVisible(ComponentNameMatcher.IME) }
     }
 
     @Presubmit
     @Test
     fun appLayerReplacesLauncher() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(ComponentNameMatcher.LAUNCHER)
                 .then()
                 .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -141,9 +139,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0))
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
index 0ca6457..a6bbf54 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
@@ -19,19 +19,18 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -49,9 +48,9 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Presubmit
-open class SwitchImeWindowsFromGestureNavTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+open class SwitchImeWindowsFromGestureNavTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = SimpleAppHelper(instrumentation)
-    private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+    private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     @Before
     open fun before() {
@@ -63,7 +62,7 @@
         setup {
             tapl.setExpectedRotationCheckEnabled(false)
             tapl.setIgnoreTaskbarVisibility(true)
-            this.setRotation(testSpec.startRotation)
+            this.setRotation(flicker.scenario.startRotation)
             testApp.launchViaIntent(wmHelper)
             wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
 
@@ -143,7 +142,7 @@
     @Presubmit
     @Test
     fun imeAppWindowVisibility() {
-        testSpec.assertWm {
+        flicker.assertWm {
             isAppWindowVisible(imeTestApp)
                 .then()
                 .isAppSnapshotStartingWindowVisibleFor(testApp, isOptional = true)
@@ -159,27 +158,25 @@
     @FlakyTest(bugId = 244414110)
     @Test
     open fun imeLayerIsVisibleWhenSwitchingToImeApp() {
-        testSpec.assertLayersStart { isVisible(ComponentNameMatcher.IME) }
-        testSpec.assertLayersTag(TAG_IME_VISIBLE) { isVisible(ComponentNameMatcher.IME) }
-        testSpec.assertLayersEnd { isVisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersStart { isVisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersTag(TAG_IME_VISIBLE) { isVisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersEnd { isVisible(ComponentNameMatcher.IME) }
     }
 
     @Presubmit
     @Test
     fun imeLayerIsInvisibleWhenSwitchingToTestApp() {
-        testSpec.assertLayersTag(TAG_IME_INVISIBLE) { isInvisible(ComponentNameMatcher.IME) }
+        flicker.assertLayersTag(TAG_IME_INVISIBLE) { isInvisible(ComponentNameMatcher.IME) }
     }
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedNavigationModes =
-                        listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY),
-                    supportedRotations = listOf(Surface.ROTATION_0)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL),
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
 
         private const val TAG_IME_VISIBLE = "imeVisible"
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
index 80ab016..c599b10 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
@@ -18,10 +18,11 @@
 
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd
+import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -39,16 +40,14 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchImeWindowsFromGestureNavTest_ShellTransit(testSpec: FlickerTestParameter) :
-    SwitchImeWindowsFromGestureNavTest(testSpec) {
+class SwitchImeWindowsFromGestureNavTest_ShellTransit(flicker: FlickerTest) :
+    SwitchImeWindowsFromGestureNavTest(flicker) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
     }
 
-    @Presubmit
-    @Test
-    override fun entireScreenCovered() = super.entireScreenCovered()
+    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     @Presubmit
     @Test
@@ -71,13 +70,13 @@
     override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /**
-     * Checks that [ComponentMatcher.NAV_BAR] window is visible and above the app windows at the
+     * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
      * start and end of the WM trace
      */
     @Presubmit
     @Test
     fun navBarWindowIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarWindowIsVisibleAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsVisibleAtStartAndEnd()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt
index 49bf86d0..5e50b45 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt
@@ -20,11 +20,11 @@
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.TwoActivitiesAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
@@ -57,13 +57,13 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ActivitiesTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+class ActivitiesTransitionTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp: TwoActivitiesAppHelper = TwoActivitiesAppHelper(instrumentation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
-            tapl.setExpectedRotation(testSpec.startRotation)
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
             testApp.launchViaIntent(wmHelper)
         }
         teardown { testApp.exit(wmHelper) }
@@ -91,7 +91,7 @@
             ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent()
         val imeAutoFocusActivityComponent =
             ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent()
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowOnTop(buttonActivityComponent)
                 .then()
                 .isAppWindowOnTop(imeAutoFocusActivityComponent)
@@ -108,7 +108,7 @@
     @Presubmit
     @Test
     fun launcherWindowNotOnTop() {
-        testSpec.assertWm { this.isAppWindowNotOnTop(ComponentNameMatcher.LAUNCHER) }
+        flicker.assertWm { this.isAppWindowNotOnTop(ComponentNameMatcher.LAUNCHER) }
     }
 
     /**
@@ -117,20 +117,20 @@
     @Presubmit
     @Test
     fun launcherLayerNotVisible() {
-        testSpec.assertLayers { this.isInvisible(ComponentNameMatcher.LAUNCHER) }
+        flicker.assertLayers { this.isInvisible(ComponentNameMatcher.LAUNCHER) }
     }
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt
index d0d7bbb..14a6668 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt
@@ -17,12 +17,12 @@
 package com.android.server.wm.flicker.launch
 
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.CameraAppHelper
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -41,8 +41,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppAfterCameraTest(testSpec: FlickerTestParameter) :
-    OpenAppFromLauncherTransition(testSpec) {
+open class OpenAppAfterCameraTest(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
     @Before
     open fun before() {
         Assume.assumeFalse(isShellTransitionsEnabled)
@@ -69,13 +68,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
index 5686965..99574ef 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
@@ -18,9 +18,9 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -40,8 +40,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppAfterCameraTest_ShellTransit(testSpec: FlickerTestParameter) :
-    OpenAppAfterCameraTest(testSpec) {
+class OpenAppAfterCameraTest_ShellTransit(flicker: FlickerTest) : OpenAppAfterCameraTest(flicker) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt
index 5e6fc21..e0df5be 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt
@@ -18,13 +18,12 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -55,17 +54,16 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppColdFromIcon(testSpec: FlickerTestParameter) :
-    OpenAppFromLauncherTransition(testSpec) {
+class OpenAppColdFromIcon(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
             setup {
-                if (testSpec.isTablet) {
-                    tapl.setExpectedRotation(testSpec.startRotation)
+                if (flicker.scenario.isTablet) {
+                    tapl.setExpectedRotation(flicker.scenario.startRotation.value)
                 } else {
-                    tapl.setExpectedRotation(Surface.ROTATION_0)
+                    tapl.setExpectedRotation(PlatformConsts.Rotation.ROTATION_0.value)
                 }
                 RemoveAllTasksButHomeRule.removeAllTasksButHome()
             }
@@ -180,19 +178,16 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                // TAPL fails on landscape mode b/240916028
-                .getConfigNonRotationTests(
-                    supportedNavigationModes = listOf(
-                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY
-                    )
-                )
+        fun getParams(): Collection<FlickerTest> {
+            // TAPL fails on landscape mode b/240916028
+            return FlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_3BUTTON)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
index 7576ab9..66af72e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
@@ -19,12 +19,12 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.annotation.FlickerServiceCompatible
-import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -57,15 +57,14 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppColdTest(testSpec: FlickerTestParameter) :
-    OpenAppFromLauncherTransition(testSpec) {
+open class OpenAppColdTest(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
             setup {
                 removeAllTasksButHome()
-                this.setRotation(testSpec.startRotation)
+                this.setRotation(flicker.scenario.startRotation)
             }
             teardown { testApp.exit(wmHelper) }
             transitions { testApp.launchViaIntent(wmHelper) }
@@ -83,13 +82,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt
index 23748be..b234ec7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt
@@ -17,28 +17,27 @@
 package com.android.server.wm.flicker.launch
 
 import android.platform.test.annotations.Presubmit
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.replacesLayer
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.Test
 
 /** Base class for app launch tests */
-abstract class OpenAppFromLauncherTransition(testSpec: FlickerTestParameter) :
-    OpenAppTransition(testSpec) {
+abstract class OpenAppFromLauncherTransition(flicker: FlickerTest) : OpenAppTransition(flicker) {
 
-    /** Checks that the focus changes from the [ComponentMatcher.LAUNCHER] to [testApp] */
+    /** Checks that the focus changes from the [ComponentNameMatcher.LAUNCHER] to [testApp] */
     @Presubmit
     @Test
     open fun focusChanges() {
-        testSpec.assertEventLog { this.focusChanges("NexusLauncherActivity", testApp.`package`) }
+        flicker.assertEventLog { this.focusChanges("NexusLauncherActivity", testApp.`package`) }
     }
 
     /**
-     * Checks that [ComponentMatcher.LAUNCHER] layer is visible at the start of the transition, and
-     * is replaced by [testApp], which remains visible until the end
+     * Checks that [ComponentNameMatcher.LAUNCHER] layer is visible at the start of the transition,
+     * and is replaced by [testApp], which remains visible until the end
      */
     open fun appLayerReplacesLauncher() {
-        testSpec.replacesLayer(
+        flicker.replacesLayer(
             ComponentNameMatcher.LAUNCHER,
             testApp,
             ignoreEntriesWithRotationLayer = true,
@@ -48,14 +47,14 @@
     }
 
     /**
-     * Checks that [ComponentMatcher.LAUNCHER] window is the top window at the start of the
-     * transition, and is replaced by a [ComponentMatcher.SNAPSHOT] or
-     * [ComponentMatcher.SPLASH_SCREEN], or [testApp], which remains visible until the end
+     * Checks that [ComponentNameMatcher.LAUNCHER] window is the top window at the start of the
+     * transition, and is replaced by a [ComponentNameMatcher.SNAPSHOT] or
+     * [ComponentNameMatcher.SPLASH_SCREEN], or [testApp], which remains visible until the end
      */
     @Presubmit
     @Test
     open fun appWindowReplacesLauncherAsTopWindow() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER)
                 .then()
                 .isAppWindowOnTop(
@@ -68,6 +67,6 @@
     @Presubmit
     @Test
     open fun appWindowAsTopWindowAtEnd() {
-        testSpec.assertWmEnd { this.isAppWindowOnTop(testApp) }
+        flicker.assertWmEnd { this.isAppWindowOnTop(testApp) }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
index 0edbc86..f5f7190 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
@@ -20,10 +20,10 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.FixMethodOrder
 import org.junit.Ignore
@@ -44,8 +44,8 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Postsubmit
-open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) :
-    OpenAppFromNotificationCold(testSpec) {
+open class OpenAppFromLockNotificationCold(flicker: FlickerTest) :
+    OpenAppFromNotificationCold(flicker) {
 
     override val openingNotificationsFromLockScreen = true
 
@@ -93,8 +93,9 @@
      * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
      * transition
      */
-    @Presubmit @Test override fun statusBarLayerPositionAtEnd() =
-        super.statusBarLayerPositionAtEnd()
+    @Presubmit
+    @Test
+    override fun statusBarLayerPositionAtEnd() = super.statusBarLayerPositionAtEnd()
 
     /** {@inheritDoc} */
     @Test
@@ -119,13 +120,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
index 5a7b8b9..fe49c61 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
@@ -19,10 +19,10 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.FixMethodOrder
@@ -43,8 +43,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) :
-    OpenAppFromNotificationWarm(testSpec) {
+class OpenAppFromLockNotificationWarm(flicker: FlickerTest) : OpenAppFromNotificationWarm(flicker) {
 
     override val openingNotificationsFromLockScreen = true
 
@@ -70,7 +69,7 @@
     @Test
     @Presubmit
     fun appWindowBecomesFirstAndOnlyTopWindow() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.hasNoVisibleAppWindow()
                 .then()
                 .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -85,7 +84,7 @@
     @Test
     @Presubmit
     fun screenLockedStart() {
-        testSpec.assertWmStart { isKeyguardShowing() }
+        flicker.assertWmStart { isKeyguardShowing() }
     }
 
     /** {@inheritDoc} */
@@ -108,7 +107,7 @@
      * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
      * transition
      */
-    @Presubmit @Test fun statusBarLayerPositionAtEnd() = testSpec.statusBarLayerPositionAtEnd()
+    @Presubmit @Test fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
 
     /** {@inheritDoc} */
     @Test
@@ -133,13 +132,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
index 4ee1283..d9a3ad2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
@@ -20,12 +20,12 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.ShowWhenLockedAppHelper
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -44,8 +44,8 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Postsubmit
-class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParameter) :
-    OpenAppFromLockNotificationCold(testSpec) {
+class OpenAppFromLockNotificationWithLockOverlayApp(flicker: FlickerTest) :
+    OpenAppFromLockNotificationCold(flicker) {
     private val showWhenLockedApp: ShowWhenLockedAppHelper =
         ShowWhenLockedAppHelper(instrumentation)
 
@@ -74,7 +74,7 @@
     @Test
     @FlakyTest(bugId = 227143265)
     fun showWhenLockedAppWindowBecomesVisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.hasNoVisibleAppWindow()
                 .then()
                 .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -86,7 +86,7 @@
     @Test
     @FlakyTest(bugId = 227143265)
     fun showWhenLockedAppLayerBecomesVisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isInvisible(showWhenLockedApp)
                 .then()
                 .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -107,20 +107,19 @@
     /** {@inheritDoc} */
     @FlakyTest(bugId = 209599395)
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() =
-        super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt
index 3cc2390..718c6e9 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt
@@ -18,8 +18,8 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.navBarLayerPositionAtEnd
 import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
 import com.android.server.wm.traces.common.ComponentNameMatcher
@@ -28,8 +28,7 @@
 import org.junit.Test
 
 /** Base class for app launch tests from lock screen */
-abstract class OpenAppFromLockTransition(testSpec: FlickerTestParameter) :
-    OpenAppTransition(testSpec) {
+abstract class OpenAppFromLockTransition(flicker: FlickerTest) : OpenAppTransition(flicker) {
 
     /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -46,7 +45,7 @@
     @Presubmit
     @Test
     open fun focusChanges() {
-        testSpec.assertEventLog { this.focusChanges("", testApp.`package`) }
+        flicker.assertEventLog { this.focusChanges("", testApp.`package`) }
     }
 
     /**
@@ -56,7 +55,7 @@
     @FlakyTest(bugId = 203538234)
     @Test
     open fun appWindowBecomesFirstAndOnlyTopWindow() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.hasNoVisibleAppWindow()
                 .then()
                 .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -71,7 +70,7 @@
     @Presubmit
     @Test
     fun screenLockedStart() {
-        testSpec.assertLayersStart { isEmpty() }
+        flicker.assertLayersStart { isEmpty() }
     }
 
     /** {@inheritDoc} */
@@ -99,16 +98,16 @@
     @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
     override fun taskBarWindowIsAlwaysVisible() {}
 
-    /** Checks the position of the [ComponentMatcher.NAV_BAR] at the end of the transition */
+    /** Checks the position of the [ComponentNameMatcher.NAV_BAR] at the end of the transition */
     @Presubmit
     @Test
     open fun navBarLayerPositionAtEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarLayerPositionAtEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerPositionAtEnd()
     }
 
-    /** Checks the position of the [ComponentMatcher.STATUS_BAR] at the end of the transition */
-    @Presubmit @Test fun statusBarLayerPositionAtEnd() = testSpec.statusBarLayerPositionAtEnd()
+    /** Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the end of the transition */
+    @Presubmit @Test fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
 
     /** {@inheritDoc} */
     @Test
@@ -116,13 +115,13 @@
     override fun statusBarLayerIsVisibleAtStartAndEnd() {}
 
     /**
-     * Checks that the [ComponentMatcher.STATUS_BAR] layer is visible at the end of the trace
+     * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible at the end of the trace
      *
      * It is not possible to check at the start because the screen is off
      */
     @Presubmit
     @Test
     fun statusBarLayerIsVisibleAtEnd() {
-        testSpec.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt
index 8dd94cd..240e90b 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt
@@ -19,10 +19,10 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.FixMethodOrder
@@ -44,8 +44,8 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Postsubmit
-open class OpenAppFromNotificationCold(testSpec: FlickerTestParameter) :
-    OpenAppFromNotificationWarm(testSpec) {
+open class OpenAppFromNotificationCold(flicker: FlickerTest) :
+    OpenAppFromNotificationWarm(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -61,13 +61,9 @@
             }
         }
 
-    @Postsubmit
-    @Test
-    override fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart()
+    @Postsubmit @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart()
 
-    @Postsubmit
-    @Test
-    override fun appLayerBecomesVisible() = appLayerBecomesVisible_coldStart()
+    @Postsubmit @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_coldStart()
 
     /** {@inheritDoc} */
     @Test
@@ -89,9 +85,7 @@
      * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
      * transition
      */
-    @Presubmit
-    @Test
-    open fun statusBarLayerPositionAtEnd() = testSpec.statusBarLayerPositionAtEnd()
+    @Presubmit @Test open fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
 
     /** {@inheritDoc} */
     @Test
@@ -107,13 +101,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt
index db48b3f..6388a5a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt
@@ -24,13 +24,13 @@
 import android.view.WindowManager
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.NotificationAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.navBarLayerIsVisibleAtEnd
 import com.android.server.wm.flicker.navBarLayerPositionAtEnd
 import com.android.server.wm.flicker.navBarWindowIsVisibleAtEnd
@@ -56,8 +56,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) :
-    OpenAppTransition(testSpec) {
+open class OpenAppFromNotificationWarm(flicker: FlickerTest) : OpenAppTransition(flicker) {
     override val testApp: NotificationAppHelper = NotificationAppHelper(instrumentation)
 
     open val openingNotificationsFromLockScreen = false
@@ -67,7 +66,7 @@
         get() = {
             setup {
                 device.wakeUpAndGoToHomeScreen()
-                this.setRotation(testSpec.startRotation)
+                this.setRotation(flicker.scenario.startRotation)
                 testApp.launchViaIntent(wmHelper)
                 wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
                 testApp.postNotification(wmHelper)
@@ -120,19 +119,19 @@
     @Presubmit
     @Test
     open fun notificationAppWindowVisibleAtEnd() {
-        testSpec.assertWmEnd { this.isAppWindowVisible(testApp) }
+        flicker.assertWmEnd { this.isAppWindowVisible(testApp) }
     }
 
     @Presubmit
     @Test
     open fun notificationAppWindowOnTopAtEnd() {
-        testSpec.assertWmEnd { this.isAppWindowOnTop(testApp) }
+        flicker.assertWmEnd { this.isAppWindowOnTop(testApp) }
     }
 
     @Presubmit
     @Test
     open fun notificationAppLayerVisibleAtEnd() {
-        testSpec.assertLayersEnd { this.isVisible(testApp) }
+        flicker.assertLayersEnd { this.isVisible(testApp) }
     }
 
     /**
@@ -144,8 +143,8 @@
     @Presubmit
     @Test
     open fun taskBarWindowIsVisibleAtEnd() {
-        Assume.assumeTrue(testSpec.isTablet)
-        testSpec.taskBarWindowIsVisibleAtEnd()
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.taskBarWindowIsVisibleAtEnd()
     }
 
     /**
@@ -156,31 +155,31 @@
     @Presubmit
     @Test
     open fun taskBarLayerIsVisibleAtEnd() {
-        Assume.assumeTrue(testSpec.isTablet)
-        testSpec.taskBarLayerIsVisibleAtEnd()
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.taskBarLayerIsVisibleAtEnd()
     }
 
     /** Checks the position of the [ComponentNameMatcher.NAV_BAR] at the end of the transition */
     @Presubmit
     @Test
     open fun navBarLayerPositionAtEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarLayerPositionAtEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerPositionAtEnd()
     }
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
     open fun navBarLayerIsVisibleAtEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarLayerIsVisibleAtEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerIsVisibleAtEnd()
     }
 
     @Presubmit
     @Test
     open fun navBarWindowIsVisibleAtEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarWindowIsVisibleAtEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsVisibleAtEnd()
     }
 
     /** {@inheritDoc} */
@@ -203,13 +202,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
index fd8a38c..9106835 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
@@ -19,13 +19,13 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.annotation.FlickerServiceCompatible
-import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -59,8 +59,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppFromOverviewTest(testSpec: FlickerTestParameter) :
-    OpenAppFromLauncherTransition(testSpec) {
+open class OpenAppFromOverviewTest(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
 
     /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
@@ -72,14 +71,14 @@
                 tapl.goHome()
                 wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
                 // By default, launcher doesn't rotate on phones, but rotates on tablets
-                if (testSpec.isTablet) {
-                    tapl.setExpectedRotation(testSpec.startRotation)
+                if (flicker.scenario.isTablet) {
+                    tapl.setExpectedRotation(flicker.scenario.startRotation.value)
                 } else {
-                    tapl.setExpectedRotation(Surface.ROTATION_0)
+                    tapl.setExpectedRotation(PlatformConsts.Rotation.ROTATION_0.value)
                 }
                 tapl.workspace.switchToOverview()
                 wmHelper.StateSyncBuilder().withRecentsActivityVisible().waitForAndVerify()
-                this.setRotation(testSpec.startRotation)
+                this.setRotation(flicker.scenario.startRotation)
             }
             transitions {
                 tapl.overview.currentTask.open()
@@ -109,13 +108,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
index 1ecde46..f295ce3 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
@@ -20,14 +20,13 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.annotation.FlickerServiceCompatible
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Ignore
@@ -63,8 +62,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppNonResizeableTest(testSpec: FlickerTestParameter) :
-    OpenAppFromLockTransition(testSpec) {
+open class OpenAppNonResizeableTest(flicker: FlickerTest) : OpenAppFromLockTransition(flicker) {
     override val testApp = NonResizeableAppHelper(instrumentation)
 
     /**
@@ -74,8 +72,8 @@
     @FlakyTest(bugId = 227083463)
     @Test
     fun navBarLayerVisibilityChanges() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.assertLayers {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.assertLayers {
             this.isInvisible(ComponentNameMatcher.NAV_BAR)
                 .then()
                 .isVisible(ComponentNameMatcher.NAV_BAR)
@@ -86,7 +84,7 @@
     @Presubmit
     @Test
     fun appWindowBecomesVisibleAtEnd() {
-        testSpec.assertWmEnd { this.isAppWindowVisible(testApp) }
+        flicker.assertWmEnd { this.isAppWindowVisible(testApp) }
     }
 
     /**
@@ -96,8 +94,8 @@
     @Presubmit
     @Test
     fun navBarWindowsVisibilityChanges() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.assertWm {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.assertWm {
             this.isNonAppWindowInvisible(ComponentNameMatcher.NAV_BAR)
                 .then()
                 .isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR)
@@ -111,8 +109,8 @@
     @Presubmit
     @Test
     fun taskBarLayerIsVisibleAtEnd() {
-        Assume.assumeTrue(testSpec.isTablet)
-        testSpec.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
     }
 
     /**
@@ -123,45 +121,40 @@
     @Presubmit
     @Test
     override fun statusBarLayerIsVisibleAtStartAndEnd() {
-        testSpec.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
     }
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun taskBarLayerIsVisibleAtStartAndEnd() {
-    }
+    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun navBarLayerIsVisibleAtStartAndEnd() {
-    }
+    override fun navBarLayerIsVisibleAtStartAndEnd() {}
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun taskBarWindowIsAlwaysVisible() {
-    }
+    override fun taskBarWindowIsAlwaysVisible() {}
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun navBarWindowIsAlwaysVisible() {
-    }
+    override fun navBarWindowIsAlwaysVisible() {}
 
     /** {@inheritDoc} */
     @Test
     @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun statusBarWindowIsAlwaysVisible() {
-    }
+    override fun statusBarWindowIsAlwaysVisible() {}
 
     /** Checks the [ComponentNameMatcher.NAV_BAR] is visible at the end of the transition */
     @Postsubmit
     @Test
     fun navBarLayerIsVisibleAtEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.assertLayersEnd { this.isVisible(ComponentNameMatcher.NAV_BAR) }
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.NAV_BAR) }
     }
 
     /** {@inheritDoc} */
@@ -174,7 +167,7 @@
     @Presubmit
     @Test
     override fun appLayerBecomesVisible() {
-        Assume.assumeFalse(testSpec.isTablet)
+        Assume.assumeFalse(flicker.scenario.isTablet)
         super.appLayerBecomesVisible()
     }
 
@@ -182,14 +175,12 @@
     @FlakyTest(bugId = 227143265)
     @Test
     fun appLayerBecomesVisibleTablet() {
-        Assume.assumeTrue(testSpec.isTablet)
+        Assume.assumeTrue(flicker.scenario.isTablet)
         super.appLayerBecomesVisible()
     }
 
     /** {@inheritDoc} */
-    @FlakyTest
-    @Test
-    override fun entireScreenCovered() = super.entireScreenCovered()
+    @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
     @FlakyTest(bugId = 218470989)
     @Test
@@ -210,18 +201,16 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedNavigationModes =
-                        listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY),
-                    supportedRotations = listOf(Surface.ROTATION_0)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL),
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
index 4fd251a..2adb0b4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
@@ -18,8 +18,8 @@
 
 import android.platform.test.annotations.Presubmit
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.StandardAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
@@ -28,15 +28,15 @@
 import org.junit.Test
 
 /** Base class for app launch tests */
-abstract class OpenAppTransition(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+abstract class OpenAppTransition(flicker: FlickerTest) : BaseTest(flicker) {
     protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
-            tapl.setExpectedRotation(testSpec.startRotation)
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
             device.wakeUpAndGoToHomeScreen()
-            this.setRotation(testSpec.startRotation)
+            this.setRotation(flicker.scenario.startRotation)
         }
         teardown { testApp.exit(wmHelper) }
     }
@@ -52,7 +52,7 @@
     }
 
     protected fun appLayerBecomesVisible_coldStart() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.notContains(testApp)
                 .then()
                 .isInvisible(testApp, isOptional = true)
@@ -66,7 +66,7 @@
     }
 
     protected fun appLayerBecomesVisible_warmStart() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isInvisible(testApp)
                 .then()
                 .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -87,7 +87,7 @@
     @Presubmit @Test open fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart()
 
     protected fun appWindowBecomesVisible_coldStart() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.notContains(testApp)
                 .then()
                 .isAppWindowInvisible(testApp, isOptional = true)
@@ -97,7 +97,7 @@
     }
 
     protected fun appWindowBecomesVisible_warmStart() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowInvisible(testApp)
                 .then()
                 .isAppWindowVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -115,7 +115,7 @@
     @Presubmit
     @Test
     open fun appWindowBecomesTopWindow() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowNotOnTop(testApp)
                 .then()
                 .isAppWindowOnTop(
@@ -131,6 +131,6 @@
     @Presubmit
     @Test
     open fun appWindowIsTopWindowAtEnd() {
-        testSpec.assertWmEnd { this.isAppWindowOnTop(testApp) }
+        flicker.assertWmEnd { this.isAppWindowOnTop(testApp) }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt
index 03741c8..62d7cc0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt
@@ -19,12 +19,12 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.annotation.FlickerServiceCompatible
-import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -57,8 +57,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppWarmTest(testSpec: FlickerTestParameter) :
-    OpenAppFromLauncherTransition(testSpec) {
+open class OpenAppWarmTest(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
     /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -68,7 +67,7 @@
                 testApp.launchViaIntent(wmHelper)
                 tapl.goHome()
                 wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
-                this.setRotation(testSpec.startRotation)
+                this.setRotation(flicker.scenario.startRotation)
             }
             teardown { testApp.exit(wmHelper) }
             transitions { testApp.launchViaIntent(wmHelper) }
@@ -96,13 +95,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
index e1fd5a7..b9594a1 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
@@ -22,16 +22,16 @@
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.server.wm.flicker.FlickerBuilderProvider
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.R
-import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.StandardAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.junit.FlickerBuilderProvider
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.WindowManagerConditionsFactory
@@ -55,7 +55,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OverrideTaskTransitionTest(val testSpec: FlickerTestParameter) {
+class OverrideTaskTransitionTest(val flicker: FlickerTest) {
 
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
@@ -66,7 +66,7 @@
             setup {
                 device.wakeUpAndGoToHomeScreen()
                 RemoveAllTasksButHomeRule.removeAllTasksButHome()
-                setRotation(testSpec.startRotation)
+                setRotation(flicker.scenario.startRotation)
             }
             transitions {
                 instrumentation.context.startActivity(
@@ -87,24 +87,24 @@
     @Presubmit
     @Test
     fun testSimpleActivityIsShownDirectly() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             // Before the app launches, only the launcher is visible.
             isVisible(ComponentNameMatcher.LAUNCHER)
-                    .isInvisible(testApp)
-                    .then()
-                    // Animation starts, but the app may not be drawn yet which means the Splash
-                    // may be visible.
-                    .isInvisible(testApp, isOptional = true)
-                    .isVisible(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
-                    .then()
-                    // App shows up with the custom animation starting at alpha=1.
-                    .isVisible(testApp)
-                    .then()
-                    // App custom animation continues to alpha=0 (invisible).
-                    .isInvisible(testApp)
-                    .then()
-                    // App custom animation ends with it being visible.
-                    .isVisible(testApp)
+                .isInvisible(testApp)
+                .then()
+                // Animation starts, but the app may not be drawn yet which means the Splash
+                // may be visible.
+                .isInvisible(testApp, isOptional = true)
+                .isVisible(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
+                .then()
+                // App shows up with the custom animation starting at alpha=1.
+                .isVisible(testApp)
+                .then()
+                // App custom animation continues to alpha=0 (invisible).
+                .isInvisible(testApp)
+                .then()
+                // App custom animation ends with it being visible.
+                .isVisible(testApp)
         }
     }
 
@@ -123,8 +123,8 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
index 08624ee..4e7ab7a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
@@ -22,13 +22,13 @@
 import android.platform.test.annotations.Postsubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.NewTasksAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.SPLASH_SCREEN
 import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER
@@ -56,7 +56,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class TaskTransitionTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = NewTasksAppHelper(instrumentation)
     private val simpleApp = SimpleAppHelper(instrumentation)
     private val wallpaper by lazy {
@@ -81,7 +81,7 @@
     @FlakyTest(bugId = 253617416)
     @Test
     fun wallpaperWindowIsNeverVisible() {
-        testSpec.assertWm { this.isNonAppWindowInvisible(wallpaper) }
+        flicker.assertWm { this.isNonAppWindowInvisible(wallpaper) }
     }
 
     /**
@@ -91,7 +91,7 @@
     @FlakyTest(bugId = 253617416)
     @Test
     fun wallpaperLayerIsNeverVisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isInvisible(wallpaper)
             this.isInvisible(WALLPAPER_BBQ_WRAPPER)
         }
@@ -104,7 +104,7 @@
     @Postsubmit
     @Test
     fun launcherWindowIsNeverVisible() {
-        testSpec.assertWm { this.isAppWindowInvisible(ComponentNameMatcher.LAUNCHER) }
+        flicker.assertWm { this.isAppWindowInvisible(ComponentNameMatcher.LAUNCHER) }
     }
 
     /**
@@ -114,7 +114,7 @@
     @Postsubmit
     @Test
     fun launcherLayerIsNeverVisible() {
-        testSpec.assertLayers { this.isInvisible(ComponentNameMatcher.LAUNCHER) }
+        flicker.assertLayers { this.isInvisible(ComponentNameMatcher.LAUNCHER) }
     }
 
     /** Checks that a color background is visible while the task transition is occurring. */
@@ -122,9 +122,9 @@
     @Test
     fun colorLayerIsVisibleDuringTransition() {
         val bgColorLayer = ComponentNameMatcher("", "colorBackgroundLayer")
-        val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
+        val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
 
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
                     it.visibleRegion(testApp.componentMatcher).coversExactly(displayBounds)
                 }
@@ -157,7 +157,7 @@
     @Postsubmit
     @Test
     fun newTaskOpensOnTopAndThenCloses() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowOnTop(testApp.componentMatcher)
                 .then()
                 .isAppWindowOnTop(SPLASH_SCREEN, isOptional = true)
@@ -235,8 +235,8 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
index bc1f0d1..b4a67ef 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
@@ -18,18 +18,17 @@
 
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -54,7 +53,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class QuickSwitchBetweenTwoAppsBackTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+open class QuickSwitchBetweenTwoAppsBackTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp1 = SimpleAppHelper(instrumentation)
     private val testApp2 = NonResizeableAppHelper(instrumentation)
 
@@ -66,7 +65,7 @@
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
-            tapl.setExpectedRotation(testSpec.startRotation)
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
             tapl.setIgnoreTaskbarVisibility(true)
             testApp1.launchViaIntent(wmHelper)
             testApp2.launchViaIntent(wmHelper)
@@ -96,7 +95,7 @@
     @Presubmit
     @Test
     open fun startsWithApp2WindowsCoverFullScreen() {
-        testSpec.assertWmStart { this.visibleRegion(testApp2).coversExactly(startDisplayBounds) }
+        flicker.assertWmStart { this.visibleRegion(testApp2).coversExactly(startDisplayBounds) }
     }
 
     /**
@@ -106,16 +105,14 @@
     @Presubmit
     @Test
     open fun startsWithApp2LayersCoverFullScreen() {
-        testSpec.assertLayersStart {
-            this.visibleRegion(testApp2).coversExactly(startDisplayBounds)
-        }
+        flicker.assertLayersStart { this.visibleRegion(testApp2).coversExactly(startDisplayBounds) }
     }
 
     /** Checks that the transition starts with [testApp2] being the top window. */
     @Presubmit
     @Test
     open fun startsWithApp2WindowBeingOnTop() {
-        testSpec.assertWmStart { this.isAppWindowOnTop(testApp2) }
+        flicker.assertWmStart { this.isAppWindowOnTop(testApp2) }
     }
 
     /**
@@ -125,7 +122,7 @@
     @Presubmit
     @Test
     open fun endsWithApp1WindowsCoveringFullScreen() {
-        testSpec.assertWmEnd { this.visibleRegion(testApp1).coversExactly(startDisplayBounds) }
+        flicker.assertWmEnd { this.visibleRegion(testApp1).coversExactly(startDisplayBounds) }
     }
 
     /**
@@ -135,7 +132,7 @@
     @Presubmit
     @Test
     fun endsWithApp1LayersCoveringFullScreen() {
-        testSpec.assertLayersEnd { this.visibleRegion(testApp1).coversExactly(startDisplayBounds) }
+        flicker.assertLayersEnd { this.visibleRegion(testApp1).coversExactly(startDisplayBounds) }
     }
 
     /**
@@ -145,7 +142,7 @@
     @Presubmit
     @Test
     open fun endsWithApp1BeingOnTop() {
-        testSpec.assertWmEnd { this.isAppWindowOnTop(testApp1) }
+        flicker.assertWmEnd { this.isAppWindowOnTop(testApp1) }
     }
 
     /**
@@ -155,7 +152,7 @@
     @Presubmit
     @Test
     open fun app1WindowBecomesAndStaysVisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowInvisible(testApp1)
                 .then()
                 .isAppWindowVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -171,7 +168,7 @@
     @Presubmit
     @Test
     open fun app1LayerBecomesAndStaysVisible() {
-        testSpec.assertLayers { this.isInvisible(testApp1).then().isVisible(testApp1) }
+        flicker.assertLayers { this.isInvisible(testApp1).then().isVisible(testApp1) }
     }
 
     /**
@@ -181,9 +178,7 @@
     @Presubmit
     @Test
     open fun app2WindowBecomesAndStaysInvisible() {
-        testSpec.assertWm {
-            this.isAppWindowVisible(testApp2).then().isAppWindowInvisible(testApp2)
-        }
+        flicker.assertWm { this.isAppWindowVisible(testApp2).then().isAppWindowInvisible(testApp2) }
     }
 
     /**
@@ -193,7 +188,7 @@
     @Presubmit
     @Test
     open fun app2LayerBecomesAndStaysInvisible() {
-        testSpec.assertLayers { this.isVisible(testApp2).then().isInvisible(testApp2) }
+        flicker.assertLayers { this.isVisible(testApp2).then().isInvisible(testApp2) }
     }
 
     /**
@@ -204,7 +199,7 @@
     @Presubmit
     @Test
     open fun app1WindowIsVisibleOnceApp2WindowIsInvisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowVisible(testApp2)
                 .then()
                 // TODO: Do we actually want to test this? Seems too implementation specific...
@@ -224,7 +219,7 @@
     @Presubmit
     @Test
     open fun app1LayerIsVisibleOnceApp2LayerIsInvisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(testApp2)
                 .then()
                 .isVisible(ComponentNameMatcher.LAUNCHER, isOptional = true)
@@ -240,13 +235,10 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedNavigationModes =
-                        listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY),
-                    supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
index f988bb2..ec4e35c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
@@ -19,9 +19,9 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.Assume
@@ -49,8 +49,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class QuickSwitchBetweenTwoAppsBackTest_ShellTransit(testSpec: FlickerTestParameter) :
-    QuickSwitchBetweenTwoAppsBackTest(testSpec) {
+open class QuickSwitchBetweenTwoAppsBackTest_ShellTransit(flicker: FlickerTest) :
+    QuickSwitchBetweenTwoAppsBackTest(flicker) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
@@ -62,21 +62,20 @@
     override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /**
-     * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at
-     * the start and end of the WM trace
+     * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
+     * start and end of the WM trace
      */
     @Presubmit
     @Test
     fun navBarWindowIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarWindowIsVisibleAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsVisibleAtStartAndEnd()
     }
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 250520840)
     @Test
-    override fun startsWithApp2LayersCoverFullScreen() =
-        super.startsWithApp2LayersCoverFullScreen()
+    override fun startsWithApp2LayersCoverFullScreen() = super.startsWithApp2LayersCoverFullScreen()
 
     @FlakyTest(bugId = 246284708)
     @Test
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
index 7e4504b..593481c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
@@ -18,18 +18,17 @@
 
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -55,8 +54,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class QuickSwitchBetweenTwoAppsForwardTest(testSpec: FlickerTestParameter) :
-    BaseTest(testSpec) {
+open class QuickSwitchBetweenTwoAppsForwardTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp1 = SimpleAppHelper(instrumentation)
     private val testApp2 = NonResizeableAppHelper(instrumentation)
 
@@ -68,7 +66,7 @@
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
-            tapl.setExpectedRotation(testSpec.startRotation)
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
 
             testApp1.launchViaIntent(wmHelper)
             testApp2.launchViaIntent(wmHelper)
@@ -105,7 +103,7 @@
     @Presubmit
     @Test
     open fun startsWithApp1WindowsCoverFullScreen() {
-        testSpec.assertWmStart {
+        flicker.assertWmStart {
             this.visibleRegion(testApp1.or(ComponentNameMatcher.LETTERBOX))
                 .coversExactly(startDisplayBounds)
         }
@@ -118,16 +116,14 @@
     @Presubmit
     @Test
     open fun startsWithApp1LayersCoverFullScreen() {
-        testSpec.assertLayersStart {
-            this.visibleRegion(testApp1).coversExactly(startDisplayBounds)
-        }
+        flicker.assertLayersStart { this.visibleRegion(testApp1).coversExactly(startDisplayBounds) }
     }
 
     /** Checks that the transition starts with [testApp1] being the top window. */
     @Presubmit
     @Test
     open fun startsWithApp1WindowBeingOnTop() {
-        testSpec.assertWmStart { this.isAppWindowOnTop(testApp1) }
+        flicker.assertWmStart { this.isAppWindowOnTop(testApp1) }
     }
 
     /**
@@ -137,7 +133,7 @@
     @Presubmit
     @Test
     open fun endsWithApp2WindowsCoveringFullScreen() {
-        testSpec.assertWmEnd { this.visibleRegion(testApp2).coversExactly(startDisplayBounds) }
+        flicker.assertWmEnd { this.visibleRegion(testApp2).coversExactly(startDisplayBounds) }
     }
 
     /**
@@ -147,7 +143,7 @@
     @Presubmit
     @Test
     open fun endsWithApp2LayersCoveringFullScreen() {
-        testSpec.assertLayersEnd {
+        flicker.assertLayersEnd {
             this.visibleRegion(testApp2.or(ComponentNameMatcher.LETTERBOX))
                 .coversExactly(startDisplayBounds)
         }
@@ -160,7 +156,7 @@
     @Presubmit
     @Test
     open fun endsWithApp2BeingOnTop() {
-        testSpec.assertWmEnd { this.isAppWindowOnTop(testApp2) }
+        flicker.assertWmEnd { this.isAppWindowOnTop(testApp2) }
     }
 
     /**
@@ -170,7 +166,7 @@
     @Presubmit
     @Test
     open fun app2WindowBecomesAndStaysVisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowInvisible(testApp2)
                 .then()
                 .isAppWindowVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -186,7 +182,7 @@
     @Presubmit
     @Test
     open fun app2LayerBecomesAndStaysVisible() {
-        testSpec.assertLayers { this.isInvisible(testApp2).then().isVisible(testApp2) }
+        flicker.assertLayers { this.isInvisible(testApp2).then().isVisible(testApp2) }
     }
 
     /**
@@ -196,9 +192,7 @@
     @Presubmit
     @Test
     open fun app1WindowBecomesAndStaysInvisible() {
-        testSpec.assertWm {
-            this.isAppWindowVisible(testApp1).then().isAppWindowInvisible(testApp1)
-        }
+        flicker.assertWm { this.isAppWindowVisible(testApp1).then().isAppWindowInvisible(testApp1) }
     }
 
     /**
@@ -208,7 +202,7 @@
     @Presubmit
     @Test
     open fun app1LayerBecomesAndStaysInvisible() {
-        testSpec.assertLayers { this.isVisible(testApp1).then().isInvisible(testApp1) }
+        flicker.assertLayers { this.isVisible(testApp1).then().isInvisible(testApp1) }
     }
 
     /**
@@ -219,7 +213,7 @@
     @Presubmit
     @Test
     open fun app2WindowIsVisibleOnceApp1WindowIsInvisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowVisible(testApp1)
                 .then()
                 .isAppWindowVisible(ComponentNameMatcher.LAUNCHER, isOptional = true)
@@ -238,7 +232,7 @@
     @Presubmit
     @Test
     open fun app2LayerIsVisibleOnceApp1LayerIsInvisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(testApp1)
                 .then()
                 .isVisible(ComponentNameMatcher.LAUNCHER, isOptional = true)
@@ -259,13 +253,10 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedNavigationModes =
-                        listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY),
-                    supportedRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
index cc954ab..477b419 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
@@ -19,9 +19,9 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.Assume
@@ -50,8 +50,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class QuickSwitchBetweenTwoAppsForwardTest_ShellTransit(testSpec: FlickerTestParameter) :
-    QuickSwitchBetweenTwoAppsForwardTest(testSpec) {
+open class QuickSwitchBetweenTwoAppsForwardTest_ShellTransit(flicker: FlickerTest) :
+    QuickSwitchBetweenTwoAppsForwardTest(flicker) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
@@ -63,14 +63,14 @@
     override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /**
-     * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at
-     * the start and end of the WM trace
+     * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
+     * start and end of the WM trace
      */
     @Presubmit
     @Test
     fun navBarWindowIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarWindowIsVisibleAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsVisibleAtStartAndEnd()
     }
 
     @FlakyTest(bugId = 246284708)
@@ -84,6 +84,5 @@
 
     @FlakyTest(bugId = 250522691)
     @Test
-    override fun startsWithApp1LayersCoverFullScreen() =
-        super.startsWithApp1LayersCoverFullScreen()
+    override fun startsWithApp1LayersCoverFullScreen() = super.startsWithApp1LayersCoverFullScreen()
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
index 3cb985a..8c8220f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
@@ -19,18 +19,17 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
-import android.view.Surface
-import android.view.WindowManagerPolicyConstants
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.service.PlatformConsts
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Ignore
@@ -55,7 +54,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class QuickSwitchFromLauncherTest(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+class QuickSwitchFromLauncherTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = SimpleAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -63,7 +62,7 @@
         setup {
             tapl.setExpectedRotationCheckEnabled(false)
 
-            tapl.setExpectedRotation(testSpec.startRotation)
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
 
             testApp.launchViaIntent(wmHelper)
             tapl.goHome()
@@ -95,7 +94,7 @@
     @Presubmit
     @Test
     fun endsWithAppWindowsCoveringFullScreen() {
-        testSpec.assertWmEnd { this.visibleRegion(testApp).coversExactly(startDisplayBounds) }
+        flicker.assertWmEnd { this.visibleRegion(testApp).coversExactly(startDisplayBounds) }
     }
 
     /**
@@ -105,7 +104,7 @@
     @Presubmit
     @Test
     fun endsWithAppLayersCoveringFullScreen() {
-        testSpec.assertLayersEnd { this.visibleRegion(testApp).coversExactly(startDisplayBounds) }
+        flicker.assertLayersEnd { this.visibleRegion(testApp).coversExactly(startDisplayBounds) }
     }
 
     /**
@@ -115,47 +114,48 @@
     @Presubmit
     @Test
     fun endsWithAppBeingOnTop() {
-        testSpec.assertWmEnd { this.isAppWindowOnTop(testApp) }
+        flicker.assertWmEnd { this.isAppWindowOnTop(testApp) }
     }
 
     /** Checks that the transition starts with the home activity being tagged as visible. */
     @Presubmit
     @Test
     fun startsWithHomeActivityFlaggedVisible() {
-        testSpec.assertWmStart { this.isHomeActivityVisible() }
+        flicker.assertWmStart { this.isHomeActivityVisible() }
     }
 
     /**
-     * Checks that the transition starts with the [ComponentMatcher.LAUNCHER] windows
+     * Checks that the transition starts with the [ComponentNameMatcher.LAUNCHER] windows
      * filling/covering exactly display size
      */
     @Presubmit
     @Test
     fun startsWithLauncherWindowsCoverFullScreen() {
-        testSpec.assertWmStart {
+        flicker.assertWmStart {
             this.visibleRegion(ComponentNameMatcher.LAUNCHER).coversExactly(startDisplayBounds)
         }
     }
 
     /**
-     * Checks that the transition starts with the [ComponentMatcher.LAUNCHER] layers
+     * Checks that the transition starts with the [ComponentNameMatcher.LAUNCHER] layers
      * filling/covering exactly the display size.
      */
     @Presubmit
     @Test
     fun startsWithLauncherLayersCoverFullScreen() {
-        testSpec.assertLayersStart {
+        flicker.assertLayersStart {
             this.visibleRegion(ComponentNameMatcher.LAUNCHER).coversExactly(startDisplayBounds)
         }
     }
 
     /**
-     * Checks that the transition starts with the [ComponentMatcher.LAUNCHER] being the top window.
+     * Checks that the transition starts with the [ComponentNameMatcher.LAUNCHER] being the top
+     * window.
      */
     @Presubmit
     @Test
     fun startsWithLauncherBeingOnTop() {
-        testSpec.assertWmStart { this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER) }
+        flicker.assertWmStart { this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER) }
     }
 
     /**
@@ -165,7 +165,7 @@
     @Presubmit
     @Test
     fun endsWithHomeActivityFlaggedInvisible() {
-        testSpec.assertWmEnd { this.isHomeActivityInvisible() }
+        flicker.assertWmEnd { this.isHomeActivityInvisible() }
     }
 
     /**
@@ -175,7 +175,7 @@
     @Presubmit
     @Test
     fun appWindowBecomesAndStaysVisible() {
-        testSpec.assertWm { this.isAppWindowInvisible(testApp).then().isAppWindowVisible(testApp) }
+        flicker.assertWm { this.isAppWindowInvisible(testApp).then().isAppWindowVisible(testApp) }
     }
 
     /**
@@ -185,18 +185,18 @@
     @Presubmit
     @Test
     fun appLayerBecomesAndStaysVisible() {
-        testSpec.assertLayers { this.isInvisible(testApp).then().isVisible(testApp) }
+        flicker.assertLayers { this.isInvisible(testApp).then().isVisible(testApp) }
     }
 
     /**
-     * Checks that the [ComponentMatcher.LAUNCHER] window starts off visible and becomes invisible
-     * at some point before the end of the transition and then stays invisible until the end of the
-     * transition.
+     * Checks that the [ComponentNameMatcher.LAUNCHER] window starts off visible and becomes
+     * invisible at some point before the end of the transition and then stays invisible until the
+     * end of the transition.
      */
     @Presubmit
     @Test
     fun launcherWindowBecomesAndStaysInvisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER)
                 .then()
                 .isAppWindowNotOnTop(ComponentNameMatcher.LAUNCHER)
@@ -204,14 +204,14 @@
     }
 
     /**
-     * Checks that the [ComponentMatcher.LAUNCHER] layer starts off visible and becomes invisible at
-     * some point before the end of the transition and then stays invisible until the end of the
-     * transition.
+     * Checks that the [ComponentNameMatcher.LAUNCHER] layer starts off visible and becomes
+     * invisible at some point before the end of the transition and then stays invisible until the
+     * end of the transition.
      */
     @Presubmit
     @Test
     fun launcherLayerBecomesAndStaysInvisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(ComponentNameMatcher.LAUNCHER)
                 .then()
                 .isInvisible(ComponentNameMatcher.LAUNCHER)
@@ -219,14 +219,14 @@
     }
 
     /**
-     * Checks that the [ComponentMatcher.LAUNCHER] window is visible at least until the app window
-     * is visible. Ensures that at any point, either the launcher or [testApp] windows are at least
-     * partially visible.
+     * Checks that the [ComponentNameMatcher.LAUNCHER] window is visible at least until the app
+     * window is visible. Ensures that at any point, either the launcher or [testApp] windows are at
+     * least partially visible.
      */
     @Presubmit
     @Test
     fun appWindowIsVisibleOnceLauncherWindowIsInvisible() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER)
                 .then()
                 .isAppWindowVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -236,14 +236,14 @@
     }
 
     /**
-     * Checks that the [ComponentMatcher.LAUNCHER] layer is visible at least until the app layer is
-     * visible. Ensures that at any point, either the launcher or [testApp] layers are at least
+     * Checks that the [ComponentNameMatcher.LAUNCHER] layer is visible at least until the app layer
+     * is visible. Ensures that at any point, either the launcher or [testApp] layers are at least
      * partially visible.
      */
     @Presubmit
     @Test
     fun appLayerIsVisibleOnceLauncherLayerIsInvisible() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(ComponentNameMatcher.LAUNCHER)
                 .then()
                 .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
@@ -263,14 +263,14 @@
     override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
     /**
-     * Checks that [ComponentMatcher.NAV_BAR] window is visible and above the app windows at the
+     * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
      * start and end of the WM trace
      */
     @Presubmit
     @Test
     fun navBarWindowIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(testSpec.isTablet)
-        testSpec.navBarWindowIsVisibleAtStartAndEnd()
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsVisibleAtStartAndEnd()
     }
 
     @Presubmit
@@ -293,14 +293,12 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance()
-                .getConfigNonRotationTests(
-                    supportedNavigationModes =
-                        listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY),
-                    // TODO: Test with 90 rotation
-                    supportedRotations = listOf(Surface.ROTATION_0)
-                )
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(PlatformConsts.NavBar.MODE_GESTURAL),
+                // TODO: Test with 90 rotation
+                supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0)
+            )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
index ad14d0d..5b52c75 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
@@ -20,11 +20,11 @@
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -78,7 +78,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ChangeAppRotationTest(testSpec: FlickerTestParameter) : RotationTransition(testSpec) {
+class ChangeAppRotationTest(flicker: FlickerTest) : RotationTransition(flicker) {
     override val testApp = SimpleAppHelper(instrumentation)
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -93,15 +93,15 @@
     @Presubmit
     @Test
     fun focusChanges() {
-        testSpec.assertEventLog { this.focusChanges(testApp.`package`) }
+        flicker.assertEventLog { this.focusChanges(testApp.`package`) }
     }
 
     /**
-     * Checks that the [ComponentMatcher.ROTATION] layer appears during the transition, doesn't
+     * Checks that the [ComponentNameMatcher.ROTATION] layer appears during the transition, doesn't
      * flicker, and disappears before the transition is complete
      */
     fun rotationLayerAppearsAndVanishesAssertion() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.isVisible(testApp)
                 .then()
                 .isVisible(ComponentNameMatcher.ROTATION)
@@ -112,7 +112,7 @@
     }
 
     /**
-     * Checks that the [ComponentMatcher.ROTATION] layer appears during the transition, doesn't
+     * Checks that the [ComponentNameMatcher.ROTATION] layer appears during the transition, doesn't
      * flicker, and disappears before the transition is complete
      */
     @Presubmit
@@ -138,13 +138,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestParameterFactory.getConfigRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
+         * See [FlickerTestFactory.rotationTests] for configuring screen orientation and navigation
+         * modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigRotationTests()
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.rotationTests()
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
index 8e3fd40..4ef9eaf 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
@@ -18,29 +18,29 @@
 
 import android.platform.test.annotations.Presubmit
 import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.helpers.StandardAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.Test
 
 /** Base class for app rotation tests */
-abstract class RotationTransition(testSpec: FlickerTestParameter) : BaseTest(testSpec) {
+abstract class RotationTransition(flicker: FlickerTest) : BaseTest(flicker) {
     protected abstract val testApp: StandardAppHelper
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
-        setup { this.setRotation(testSpec.startRotation) }
+        setup { this.setRotation(flicker.scenario.startRotation) }
         teardown { testApp.exit(wmHelper) }
-        transitions { this.setRotation(testSpec.endRotation) }
+        transitions { this.setRotation(flicker.scenario.endRotation) }
     }
 
     /** {@inheritDoc} */
     @Presubmit
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.visibleLayersShownMoreThanOneConsecutiveEntry(
                 ignoreLayers =
                     listOf(
@@ -56,7 +56,7 @@
     @Presubmit
     @Test
     open fun appLayerRotates_StartingPos() {
-        testSpec.assertLayersStart {
+        flicker.assertLayersStart {
             this.entry.displays.map { display ->
                 this.visibleRegion(testApp).coversExactly(display.layerStackSpace)
             }
@@ -67,7 +67,7 @@
     @Presubmit
     @Test
     open fun appLayerRotates_EndingPos() {
-        testSpec.assertLayersEnd {
+        flicker.assertLayersEnd {
             this.entry.displays.map { display ->
                 this.visibleRegion(testApp).coversExactly(display.layerStackSpace)
             }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
index d0d4122..54f38c3 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
@@ -21,11 +21,12 @@
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
 import android.view.WindowManager
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.FlickerTestParameterFactory
-import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.ScenarioBuilder
 import com.android.server.wm.flicker.helpers.SeamlessRotationAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import org.junit.FixMethodOrder
@@ -38,7 +39,7 @@
 /**
  * Test opening an app and cycling through app rotations using seamless rotations
  *
- * Currently runs:
+ * Currently, runs:
  * ```
  *      0 -> 90 degrees
  *      0 -> 90 degrees (with starved UI thread)
@@ -83,7 +84,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SeamlessAppRotationTest(testSpec: FlickerTestParameter) : RotationTransition(testSpec) {
+open class SeamlessAppRotationTest(flicker: FlickerTest) : RotationTransition(flicker) {
     override val testApp = SeamlessRotationAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -96,7 +97,7 @@
                     stringExtras =
                         mapOf(
                             ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD to
-                                testSpec.starveUiThread.toString()
+                                flicker.starveUiThread.toString()
                         )
                 )
             }
@@ -106,7 +107,7 @@
     @Presubmit
     @Test
     fun appWindowFullScreen() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.invoke("isFullScreen") {
                 val appWindow = it.windowState(testApp.`package`)
                 val flags = appWindow.windowState?.attributes?.flags ?: 0
@@ -122,7 +123,7 @@
     @Presubmit
     @Test
     fun appWindowSeamlessRotation() {
-        testSpec.assertWm {
+        flicker.assertWm {
             this.invoke("isRotationSeamless") {
                 val appWindow = it.windowState(testApp.`package`)
                 val rotationAnimation = appWindow.windowState?.attributes?.rotationAnimation ?: 0
@@ -142,14 +143,14 @@
     @Presubmit
     @Test
     fun appLayerAlwaysVisible() {
-        testSpec.assertLayers { isVisible(testApp) }
+        flicker.assertLayers { isVisible(testApp) }
     }
 
     /** Checks that [testApp] layer covers the entire screen during the whole transition */
     @Presubmit
     @Test
     fun appLayerRotates() {
-        testSpec.assertLayers {
+        flicker.assertLayers {
             this.invoke("entireScreenCovered") { entry ->
                 entry.entry.displays.map { display ->
                     entry.visibleRegion(testApp).coversExactly(display.layerStackSpace)
@@ -180,7 +181,7 @@
     @Presubmit
     @Test
     fun statusBarWindowIsAlwaysInvisible() {
-        testSpec.assertWm { this.isAboveAppWindowInvisible(ComponentNameMatcher.STATUS_BAR) }
+        flicker.assertWm { this.isAboveAppWindowInvisible(ComponentNameMatcher.STATUS_BAR) }
     }
 
     /**
@@ -190,14 +191,14 @@
     @Presubmit
     @Test
     fun statusBarLayerIsAlwaysInvisible() {
-        testSpec.assertLayers { this.isInvisible(ComponentNameMatcher.STATUS_BAR) }
+        flicker.assertLayers { this.isInvisible(ComponentNameMatcher.STATUS_BAR) }
     }
 
     /** Checks that the focus doesn't change during animation */
     @Presubmit
     @Test
     fun focusDoesNotChange() {
-        testSpec.assertEventLog { this.focusDoesNotChange() }
+        flicker.assertEventLog { this.focusDoesNotChange() }
     }
 
     /** {@inheritDoc} */
@@ -208,7 +209,7 @@
     @Test
     @IwTest(focusArea = "ime")
     override fun cujCompleted() {
-        if (!testSpec.isTablet) {
+        if (!flicker.scenario.isTablet) {
             // not yet tablet compatible
             appLayerRotates()
             appLayerAlwaysVisible()
@@ -231,49 +232,39 @@
     }
 
     companion object {
-        private val FlickerTestParameter.starveUiThread
+        private val FlickerTest.starveUiThread
             get() =
-                config.getOrDefault(ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD, false)
-                    as Boolean
+                getConfigValue<Boolean>(ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD)
+                    ?: false
 
-        private fun createConfig(
-            sourceConfig: FlickerTestParameter,
-            starveUiThread: Boolean
-        ): FlickerTestParameter {
-            val newConfig =
-                sourceConfig.config.toMutableMap().also {
-                    it[ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD] = starveUiThread
-                }
+        private fun createConfig(sourceConfig: FlickerTest, starveUiThread: Boolean): FlickerTest {
+            val originalScenario = sourceConfig.initialize("createConfig")
             val nameExt = if (starveUiThread) "_BUSY_UI_THREAD" else ""
-            return FlickerTestParameter(newConfig, nameOverride = "$sourceConfig$nameExt")
+            val newConfig =
+                ScenarioBuilder()
+                    .fromScenario(originalScenario)
+                    .withExtraConfig(
+                        ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD,
+                        starveUiThread
+                    )
+                    .withDescriptionOverride("${originalScenario.description}$nameExt")
+            return FlickerTest(newConfig)
         }
 
         /**
          * Creates the test configurations for seamless rotation based on the default rotation tests
-         * from [FlickerTestParameterFactory.getConfigRotationTests], but adding an additional flag
-         * ([ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD]) to indicate if the app should
+         * from [FlickerTestFactory.rotationTests], but adding a flag (
+         * [ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD]) to indicate if the app should
          * starve the UI thread of not
-         */
+        */
+        @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        private fun getConfigurations(): List<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigRotationTests().flatMap {
-                sourceConfig ->
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.rotationTests().flatMap { sourceConfig ->
                 val defaultRun = createConfig(sourceConfig, starveUiThread = false)
                 val busyUiRun = createConfig(sourceConfig, starveUiThread = true)
                 listOf(defaultRun, busyUiRun)
             }
         }
-
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestParameterFactory.getConfigRotationTests] for configuring repetitions,
-         * screen orientation and navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTestParameter> {
-            return getConfigurations()
-        }
     }
 }
diff --git a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ControllerActivity.java b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ControllerActivity.java
index e6b60cf..167d560 100644
--- a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ControllerActivity.java
+++ b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ControllerActivity.java
@@ -88,46 +88,7 @@
     }
 
     private static String insetsTypesToString(int types) {
-        if (types == 0) {
-            return "none";
-        }
-        final StringBuilder sb = new StringBuilder();
-        if ((types & Type.statusBars()) != 0) {
-            types &= ~Type.statusBars();
-            sb.append("statusBars ");
-        }
-        if ((types & Type.navigationBars()) != 0) {
-            types &= ~Type.navigationBars();
-            sb.append("navigationBars ");
-        }
-        if ((types & Type.captionBar()) != 0) {
-            types &= ~Type.captionBar();
-            sb.append("captionBar ");
-        }
-        if ((types & Type.ime()) != 0) {
-            types &= ~Type.ime();
-            sb.append("ime ");
-        }
-        if ((types & Type.systemGestures()) != 0) {
-            types &= ~Type.systemGestures();
-            sb.append("systemGestures ");
-        }
-        if ((types & Type.mandatorySystemGestures()) != 0) {
-            types &= ~Type.mandatorySystemGestures();
-            sb.append("mandatorySystemGestures ");
-        }
-        if ((types & Type.tappableElement()) != 0) {
-            types &= ~Type.tappableElement();
-            sb.append("tappableElement ");
-        }
-        if ((types & Type.displayCutout()) != 0) {
-            types &= ~Type.displayCutout();
-            sb.append("displayCutout ");
-        }
-        if (types != 0) {
-            sb.append("unknownTypes:").append(types);
-        }
-        return sb.toString();
+        return types == 0 ? "none" : WindowInsets.Type.toString(types);
     }
 
     @Override
diff --git a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java
index d85a5bd..5da18dc 100644
--- a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java
+++ b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java
@@ -393,6 +393,21 @@
         mEventHandler = new Handler(context.getMainLooper());
     }
 
+    /**
+     * Construct WifiNl80211Manager with giving context and binder which is an interface of
+     * IWificond.
+     *
+     * @param context Android context.
+     * @param binder a binder of IWificond.
+     */
+    public WifiNl80211Manager(@NonNull Context context, @NonNull IBinder binder) {
+        this(context);
+        mWificond = IWificond.Stub.asInterface(binder);
+        if (mWificond == null) {
+            Log.e(TAG, "Failed to get reference to wificond");
+        }
+    }
+
     /** @hide */
     @VisibleForTesting
     public WifiNl80211Manager(Context context, IWificond wificond) {