Merge "Revert^2 "Modernize alternate bouncer logic""
diff --git a/apct-tests/perftests/contentcapture/AndroidManifest.xml b/apct-tests/perftests/contentcapture/AndroidManifest.xml
index 80957c7..6b566af 100644
--- a/apct-tests/perftests/contentcapture/AndroidManifest.xml
+++ b/apct-tests/perftests/contentcapture/AndroidManifest.xml
@@ -18,6 +18,8 @@
 
     <application>
         <uses-library android:name="android.test.runner" />
+        <activity android:name="android.perftests.utils.PerfTestActivity"
+                android:exported="true" />
         <activity android:name="android.view.contentcapture.CustomTestActivity"
                 android:exported="true">
         </activity>
diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java
index 9b853fe..0ea2daf 100644
--- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java
+++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java
@@ -22,18 +22,20 @@
 
 import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
 
+import android.app.Activity;
 import android.app.Application;
+import android.app.Instrumentation;
 import android.content.ContentCaptureOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.os.BatteryManager;
 import android.os.UserHandle;
 import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.PerfTestActivity;
 import android.provider.Settings;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.test.rule.ActivityTestRule;
 
 import com.android.compatibility.common.util.ActivitiesWatcher;
 import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher;
@@ -53,18 +55,18 @@
 public abstract class AbstractContentCapturePerfTestCase {
 
     private static final String TAG = AbstractContentCapturePerfTestCase.class.getSimpleName();
-    private static final long GENERIC_TIMEOUT_MS = 10_000;
+    protected static final long GENERIC_TIMEOUT_MS = 5_000;
 
     private static int sOriginalStayOnWhilePluggedIn;
-    private static Context sContext = getInstrumentation().getTargetContext();
+    protected static final Instrumentation sInstrumentation = getInstrumentation();
+    protected static final Context sContext = sInstrumentation.getTargetContext();
 
     protected ActivitiesWatcher mActivitiesWatcher;
 
-    private MyContentCaptureService.ServiceWatcher mServiceWatcher;
+    /** A simple activity as the task root to reduce the noise of pause and animation time. */
+    protected Activity mEntryActivity;
 
-    @Rule
-    public ActivityTestRule<CustomTestActivity> mActivityRule =
-            new ActivityTestRule<>(CustomTestActivity.class, false, false);
+    private MyContentCaptureService.ServiceWatcher mServiceWatcher;
 
     @Rule
     public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -220,6 +222,17 @@
         }
     }
 
+    @Before
+    public void setUp() {
+        mEntryActivity = sInstrumentation.startActivitySync(
+                PerfTestActivity.createLaunchIntent(sInstrumentation.getContext()));
+    }
+
+    @After
+    public void tearDown() {
+        mEntryActivity.finishAndRemoveTask();
+    }
+
     /**
      * Sets {@link MyContentCaptureService} as the service for the current user and waits until
      * its created, then add the perf test package into allow list.
@@ -248,20 +261,24 @@
     }
 
     /**
+     * Returns the intent which will launch CustomTestActivity.
+     */
+    protected Intent getLaunchIntent(int layoutId, int numViews) {
+        final Intent intent = new Intent(sContext, CustomTestActivity.class)
+                // Use NEW_TASK because the context is not activity. It is still in the same task
+                // of PerfTestActivity because of the same task affinity. Use NO_ANIMATION because
+                // this test focuses on launch time instead of animation duration.
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
+        intent.putExtra(INTENT_EXTRA_LAYOUT_ID, layoutId);
+        intent.putExtra(INTENT_EXTRA_CUSTOM_VIEWS, numViews);
+        return intent;
+    }
+
+    /**
      * Launch test activity with give layout and parameter
      */
     protected CustomTestActivity launchActivity(int layoutId, int numViews) {
-        final Intent intent = new Intent(sContext, CustomTestActivity.class);
-        intent.putExtra(INTENT_EXTRA_LAYOUT_ID, layoutId);
-        intent.putExtra(INTENT_EXTRA_CUSTOM_VIEWS, numViews);
-        return mActivityRule.launchActivity(intent);
-    }
-
-    protected void finishActivity() {
-        try {
-            mActivityRule.finishActivity();
-        } catch (IllegalStateException e) {
-            // no op
-        }
+        final Intent intent = getLaunchIntent(layoutId, numViews);
+        return (CustomTestActivity) sInstrumentation.startActivitySync(intent);
     }
 }
diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java
index e509837..c24e79f 100644
--- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java
+++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java
@@ -19,6 +19,10 @@
 import android.app.Activity;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
+import android.os.Looper;
+import android.os.RemoteCallback;
+import android.view.Choreographer;
+import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
@@ -31,6 +35,8 @@
 public class CustomTestActivity extends Activity {
     public static final String INTENT_EXTRA_LAYOUT_ID = "layout_id";
     public static final String INTENT_EXTRA_CUSTOM_VIEWS = "custom_view_number";
+    static final String INTENT_EXTRA_FINISH_ON_IDLE = "finish";
+    static final String INTENT_EXTRA_DRAW_CALLBACK = "draw_callback";
     public static final int MAX_VIEWS = 500;
     private static final int CUSTOM_CONTAINER_LAYOUT_ID = R.layout.test_container_activity;
 
@@ -47,6 +53,34 @@
                         getIntent().getIntExtra(INTENT_EXTRA_CUSTOM_VIEWS, MAX_VIEWS));
             }
         }
+
+        final RemoteCallback drawCallback = getIntent().getParcelableExtra(
+                INTENT_EXTRA_DRAW_CALLBACK, RemoteCallback.class);
+        if (drawCallback != null) {
+            getWindow().getDecorView().addOnAttachStateChangeListener(
+                    new View.OnAttachStateChangeListener() {
+                        @Override
+                        public void onViewAttachedToWindow(View v) {
+                            Choreographer.getInstance().postCallback(
+                                    Choreographer.CALLBACK_COMMIT,
+                                    // Report that the first frame is drawn.
+                                    () -> drawCallback.sendResult(null), null /* token */);
+                        }
+
+                        @Override
+                        public void onViewDetachedFromWindow(View v) {
+                        }
+                    });
+        }
+
+        if (getIntent().getBooleanExtra(INTENT_EXTRA_FINISH_ON_IDLE, false)) {
+            Looper.myQueue().addIdleHandler(() -> {
+                // Finish without animation.
+                finish();
+                overridePendingTransition(0 /* enterAnim */, 0 /* exitAnim */);
+                return false;
+            });
+        }
     }
 
     private void createCustomViews(LinearLayout root, int number) {
diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java
index 7257509..aa95dfd 100644
--- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java
+++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java
@@ -15,9 +15,10 @@
  */
 package android.view.contentcapture;
 
-import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.CREATED;
 import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.DESTROYED;
 
+import android.content.Intent;
+import android.os.RemoteCallback;
 import android.perftests.utils.BenchmarkState;
 import android.view.View;
 
@@ -80,17 +81,32 @@
     }
 
     private void testActivityLaunchTime(int layoutId, int numViews) throws Throwable {
+        final Object drawNotifier = new Object();
+        final Intent intent = getLaunchIntent(layoutId, numViews);
+        intent.putExtra(CustomTestActivity.INTENT_EXTRA_FINISH_ON_IDLE, true);
+        intent.putExtra(CustomTestActivity.INTENT_EXTRA_DRAW_CALLBACK,
+                new RemoteCallback(result -> {
+                    synchronized (drawNotifier) {
+                        drawNotifier.notifyAll();
+                    }
+                }));
         final ActivityWatcher watcher = startWatcher();
 
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            launchActivity(layoutId, numViews);
+            mEntryActivity.startActivity(intent);
+            synchronized (drawNotifier) {
+                try {
+                    drawNotifier.wait(GENERIC_TIMEOUT_MS);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }
 
             // Ignore the time to finish the activity
             state.pauseTiming();
-            watcher.waitFor(CREATED);
-            finishActivity();
             watcher.waitFor(DESTROYED);
+            sInstrumentation.waitForIdleSync();
             state.resumeTiming();
         }
     }
@@ -142,12 +158,12 @@
         while (state.keepRunning()) {
             // Only count the time of onVisibilityAggregated()
             state.pauseTiming();
-            mActivityRule.runOnUiThread(() -> {
+            sInstrumentation.runOnMainSync(() -> {
                 state.resumeTiming();
                 view.onVisibilityAggregated(false);
                 state.pauseTiming();
             });
-            mActivityRule.runOnUiThread(() -> {
+            sInstrumentation.runOnMainSync(() -> {
                 state.resumeTiming();
                 view.onVisibilityAggregated(true);
                 state.pauseTiming();
diff --git a/apct-tests/perftests/settingsprovider/AndroidManifest.xml b/apct-tests/perftests/settingsprovider/AndroidManifest.xml
index 140c280..9509c83 100644
--- a/apct-tests/perftests/settingsprovider/AndroidManifest.xml
+++ b/apct-tests/perftests/settingsprovider/AndroidManifest.xml
@@ -15,7 +15,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.perftests.multiuser">
+        package="com.android.perftests.settingsprovider">
 
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
@@ -26,6 +26,6 @@
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.perftests.multiuser"/>
+            android:targetPackage="com.android.perftests.settingsprovider"/>
 
 </manifest>
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 9caf99e..19ab5bc 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -30,6 +30,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.app.Notification;
 import android.compat.Compatibility;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
@@ -403,6 +404,13 @@
     public static final int FLAG_DATA_TRANSFER = 1 << 5;
 
     /**
+     * Whether it's a user initiated job or not.
+     *
+     * @hide
+     */
+    public static final int FLAG_USER_INITIATED = 1 << 6;
+
+    /**
      * @hide
      */
     public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
@@ -738,6 +746,14 @@
     }
 
     /**
+     * @see JobInfo.Builder#setUserInitiated(boolean)
+     * @hide
+     */
+    public boolean isUserInitiated() {
+        return (flags & FLAG_USER_INITIATED) != 0;
+    }
+
+    /**
      * @see JobInfo.Builder#setImportantWhileForeground(boolean)
      */
     public boolean isImportantWhileForeground() {
@@ -1849,15 +1865,8 @@
          *
          * <p>
          * For user-initiated transfers that must be started immediately, call
-         * {@link #setExpedited(boolean) setExpedited(true)}. Otherwise, the system may defer the
-         * job to a more opportune time. Using {@link #setExpedited(boolean) setExpedited(true)}
-         * with this API will only be allowed for foreground apps and when the user has clearly
-         * interacted with the app. {@link #setExpedited(boolean) setExpedited(true)} will return
-         * {@link JobScheduler#RESULT_FAILURE} for a data transfer job if the app is in the
-         * background. Apps that successfully schedule data transfer jobs with
-         * {@link #setExpedited(boolean) setExpedited(true)} will not have quotas applied to them,
-         * though they may still be stopped for system health or constraint reasons. The system will
-         * also give a user the ability to stop a data transfer job via the Task Manager.
+         * {@link #setUserInitiated(boolean) setUserInitiated(true)}. Otherwise, the system may
+         * defer the job to a more opportune time.
          *
          * <p>
          * If you want to perform more than one data transfer job, consider enqueuing multiple
@@ -1877,6 +1886,50 @@
         }
 
         /**
+         * Indicates that this job is being scheduled to fulfill an explicit user request.
+         * As such, user-initiated jobs can only be scheduled when the app is in the foreground
+         * or in a state where launching an activity is allowed, as defined
+         * <a href=
+         * "https://developer.android.com/guide/components/activities/background-starts#exceptions">
+         * here</a>. Attempting to schedule one outside of these conditions will throw a
+         * {@link SecurityException}.
+         *
+         * <p>
+         * This should <b>NOT</b> be used for automatic features.
+         *
+         * <p>
+         * All user-initiated jobs must have an associated notification, set via
+         * {@link JobService#setNotification(JobParameters, int, Notification, int)}, and will be
+         * shown in the Task Manager when running.
+         *
+         * <p>
+         * These jobs will not be subject to quotas and will be started immediately once scheduled
+         * if all constraints are met and the device system health allows for additional tasks.
+         *
+         * @see JobInfo#isUserInitiated()
+         * @hide
+         */
+        @NonNull
+        public Builder setUserInitiated(boolean userInitiated) {
+            if (userInitiated) {
+                mFlags |= FLAG_USER_INITIATED;
+                if (mPriority == PRIORITY_DEFAULT) {
+                    // The default priority for UIJs is MAX, but only change this if .setPriority()
+                    // hasn't been called yet.
+                    mPriority = PRIORITY_MAX;
+                }
+            } else {
+                if (mPriority == PRIORITY_MAX && (mFlags & FLAG_USER_INITIATED) != 0) {
+                    // Reset the priority for the job, but only change this if .setPriority()
+                    // hasn't been called yet.
+                    mPriority = PRIORITY_DEFAULT;
+                }
+                mFlags &= (~FLAG_USER_INITIATED);
+            }
+            return this;
+        }
+
+        /**
          * Setting this to true indicates that this job is important while the scheduling app
          * is in the foreground or on the temporary whitelist for background restrictions.
          * This means that the system will relax doze restrictions on this job during this time.
@@ -2086,10 +2139,12 @@
         }
 
         final boolean isExpedited = (flags & FLAG_EXPEDITED) != 0;
+        final boolean isUserInitiated = (flags & FLAG_USER_INITIATED) != 0;
         switch (mPriority) {
             case PRIORITY_MAX:
-                if (!isExpedited) {
-                    throw new IllegalArgumentException("Only expedited jobs can have max priority");
+                if (!(isExpedited || isUserInitiated)) {
+                    throw new IllegalArgumentException(
+                            "Only expedited or user-initiated jobs can have max priority");
                 }
                 break;
             case PRIORITY_HIGH:
@@ -2118,14 +2173,20 @@
             if (isPeriodic) {
                 throw new IllegalArgumentException("An expedited job cannot be periodic");
             }
+            if ((flags & FLAG_DATA_TRANSFER) != 0) {
+                throw new IllegalArgumentException(
+                        "An expedited job cannot also be a data transfer job");
+            }
+            if (isUserInitiated) {
+                throw new IllegalArgumentException("An expedited job cannot be user-initiated");
+            }
             if (mPriority != PRIORITY_MAX && mPriority != PRIORITY_HIGH) {
                 throw new IllegalArgumentException(
                         "An expedited job must be high or max priority. Don't use expedited jobs"
                                 + " for unimportant tasks.");
             }
-            if (((constraintFlags & ~CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0
-                    || (flags & ~(FLAG_EXPEDITED | FLAG_EXEMPT_FROM_APP_STANDBY
-                                    | FLAG_DATA_TRANSFER)) != 0)) {
+            if ((constraintFlags & ~CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0
+                    || (flags & ~(FLAG_EXPEDITED | FLAG_EXEMPT_FROM_APP_STANDBY)) != 0) {
                 throw new IllegalArgumentException(
                         "An expedited job can only have network and storage-not-low constraints");
             }
@@ -2152,6 +2213,33 @@
                         "A data transfer job must specify a valid network type");
             }
         }
+
+        if (isUserInitiated) {
+            if (hasEarlyConstraint) {
+                throw new IllegalArgumentException("A user-initiated job cannot have a time delay");
+            }
+            if (hasLateConstraint) {
+                throw new IllegalArgumentException("A user-initiated job cannot have a deadline");
+            }
+            if (isPeriodic) {
+                throw new IllegalArgumentException("A user-initiated job cannot be periodic");
+            }
+            if ((flags & FLAG_PREFETCH) != 0) {
+                throw new IllegalArgumentException(
+                        "A user-initiated job cannot also be a prefetch job");
+            }
+            if (mPriority != PRIORITY_MAX) {
+                throw new IllegalArgumentException("A user-initiated job must be max priority.");
+            }
+            if ((constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
+                throw new IllegalArgumentException(
+                        "A user-initiated job cannot have a device-idle constraint");
+            }
+            if (triggerContentUris != null && triggerContentUris.length > 0) {
+                throw new IllegalArgumentException(
+                        "Can't call addTriggerContentUri() on a user-initiated job");
+            }
+        }
     }
 
     /**
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 30986dd..651853b 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -761,8 +761,8 @@
             if (js != null) {
                 mWorkCountTracker.incrementRunningJobCount(jsc.getRunningJobWorkType());
                 assignment.workType = jsc.getRunningJobWorkType();
-                if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
-                    info.numRunningTopEj++;
+                if (js.startedWithImmediacyPrivilege) {
+                    info.numRunningImmediacyPrivileged++;
                 }
             }
 
@@ -829,11 +829,9 @@
                 continue;
             }
 
-            final boolean isTopEj = nextPending.shouldTreatAsExpeditedJob()
-                    && nextPending.lastEvaluatedBias == JobInfo.BIAS_TOP_APP;
+            final boolean hasImmediacyPrivilege = hasImmediacyPrivilegeLocked(nextPending);
             if (DEBUG && isSimilarJobRunningLocked(nextPending)) {
-                Slog.w(TAG, "Already running similar " + (isTopEj ? "TOP-EJ" : "job")
-                        + " to: " + nextPending);
+                Slog.w(TAG, "Already running similar job to: " + nextPending);
             }
 
             // Factoring minChangedWaitingTimeMs into the min waiting time effectively limits
@@ -876,23 +874,25 @@
                     final JobStatus runningJob = assignment.context.getRunningJobLocked();
                     // Maybe stop the job if it has had its day in the sun. Only allow replacing
                     // for one of the following conditions:
-                    // 1. We're putting in the current TOP app's EJ
+                    // 1. We're putting in a job that has the privilege of running immediately
                     // 2. There aren't too many jobs running AND the current job started when the
                     //    app was in the background
                     // 3. There aren't too many jobs running AND the current job started when the
                     //    app was on TOP, but the app has since left TOP
                     // 4. There aren't too many jobs running AND the current job started when the
-                    //    app was on TOP, the app is still TOP, but there are too many TOP+EJs
+                    //    app was on TOP, the app is still TOP, but there are too many
+                    //    immediacy-privileged jobs
                     //    running (because we don't want them to starve out other apps and the
                     //    current job has already run for the minimum guaranteed time).
                     // 5. This new job could be waiting for too long for a slot to open up
-                    boolean canReplace = isTopEj; // Case 1
+                    boolean canReplace = hasImmediacyPrivilege; // Case 1
                     if (!canReplace && !isInOverage) {
                         final int currentJobBias = mService.evaluateJobBiasLocked(runningJob);
                         canReplace = runningJob.lastEvaluatedBias < JobInfo.BIAS_TOP_APP // Case 2
                                 || currentJobBias < JobInfo.BIAS_TOP_APP // Case 3
                                 // Case 4
-                                || info.numRunningTopEj > .5 * mWorkTypeConfig.getMaxTotal();
+                                || info.numRunningImmediacyPrivileged
+                                        > (mWorkTypeConfig.getMaxTotal() / 2);
                     }
                     if (!canReplace && mMaxWaitTimeBypassEnabled) { // Case 5
                         if (nextPending.shouldTreatAsExpeditedJob()) {
@@ -919,7 +919,7 @@
                     }
                 }
             }
-            if (selectedContext == null && (!isInOverage || isTopEj)) {
+            if (selectedContext == null && (!isInOverage || hasImmediacyPrivilege)) {
                 int lowestBiasSeen = Integer.MAX_VALUE;
                 long newMinPreferredUidOnlyWaitingTimeMs = Long.MAX_VALUE;
                 for (int p = preferredUidOnly.size() - 1; p >= 0; --p) {
@@ -962,12 +962,13 @@
                     info.minPreferredUidOnlyWaitingTimeMs = newMinPreferredUidOnlyWaitingTimeMs;
                 }
             }
-            // Make sure to run EJs for the TOP app immediately.
-            if (isTopEj) {
+            // Make sure to run jobs with special privilege immediately.
+            if (hasImmediacyPrivilege) {
                 if (selectedContext != null
                         && selectedContext.context.getRunningJobLocked() != null) {
-                    // We're "replacing" a currently running job, but we want TOP EJs to start
-                    // immediately, so we'll start the EJ on a fresh available context and
+                    // We're "replacing" a currently running job, but we want immediacy-privileged
+                    // jobs to start immediately, so we'll start the privileged jobs on a fresh
+                    // available context and
                     // stop this currently running job to replace in two steps.
                     changed.add(selectedContext);
                     projectedRunningCount--;
@@ -1029,6 +1030,7 @@
                     projectedRunningCount--;
                 }
                 if (selectedContext.newJob != null) {
+                    selectedContext.newJob.startedWithImmediacyPrivilege = hasImmediacyPrivilege;
                     projectedRunningCount++;
                     minChangedWaitingTimeMs = Math.min(minChangedWaitingTimeMs,
                             mService.getMinJobExecutionGuaranteeMs(selectedContext.newJob));
@@ -1103,6 +1105,18 @@
         mActivePkgStats.forEach(mPackageStatsStagingCountClearer);
     }
 
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    boolean hasImmediacyPrivilegeLocked(@NonNull JobStatus job) {
+        // EJs & user-initiated jobs for the TOP app should run immediately.
+        // However, even for user-initiated jobs, if the app has not recently been in TOP or BAL
+        // state, we don't give the immediacy privilege so that we can try and maintain
+        // reasonably concurrency behavior.
+        return job.lastEvaluatedBias == JobInfo.BIAS_TOP_APP
+                // TODO(): include BAL state for user-initiated jobs
+                && (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiated());
+    }
+
     @GuardedBy("mLock")
     void onUidBiasChangedLocked(int prevBias, int newBias) {
         if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) {
@@ -1361,7 +1375,7 @@
         mActiveServices.remove(worker);
         if (mIdleContexts.size() < MAX_RETAINED_OBJECTS) {
             // Don't need to save all new contexts, but keep some extra around in case we need
-            // extras for another TOP+EJ overage.
+            // extras for another immediacy privileged overage.
             mIdleContexts.add(worker);
         } else {
             mNumDroppedContexts++;
@@ -1403,7 +1417,8 @@
             }
             if (respectConcurrencyLimit) {
                 worker.clearPreferredUid();
-                // We're over the limit (because the TOP app scheduled a lot of EJs), but we should
+                // We're over the limit (because there were a lot of immediacy-privileged jobs
+                // scheduled), but we should
                 // be able to stop the other jobs soon so don't start running anything new until we
                 // get back below the limit.
                 noteConcurrency();
@@ -1627,17 +1642,17 @@
                 }
             } else if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) {
                 return "blocking " + workTypeToString(WORK_TYPE_EJ) + " queue";
-            } else if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
-                // Try not to let TOP + EJ starve out other apps.
-                int topEjCount = 0;
+            } else if (js.startedWithImmediacyPrivilege) {
+                // Try not to let jobs with immediacy privilege starve out other apps.
+                int immediacyPrivilegeCount = 0;
                 for (int r = mRunningJobs.size() - 1; r >= 0; --r) {
                     JobStatus j = mRunningJobs.valueAt(r);
-                    if (j.startedAsExpeditedJob && j.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
-                        topEjCount++;
+                    if (j.startedWithImmediacyPrivilege) {
+                        immediacyPrivilegeCount++;
                     }
                 }
-                if (topEjCount > .5 * mWorkTypeConfig.getMaxTotal()) {
-                    return "prevent top EJ dominance";
+                if (immediacyPrivilegeCount > mWorkTypeConfig.getMaxTotal() / 2) {
+                    return "prevent immediacy privilege dominance";
                 }
             }
             // No other pending EJs. Return null so we don't let regular jobs preempt an EJ.
@@ -1688,6 +1703,40 @@
         return foundSome;
     }
 
+    /**
+     * Returns the estimated network bytes if the job is running. Returns {@code null} if the job
+     * isn't running.
+     */
+    @Nullable
+    @GuardedBy("mLock")
+    Pair<Long, Long> getEstimatedNetworkBytesLocked(String pkgName, int uid, int jobId) {
+        for (int i = 0; i < mActiveServices.size(); i++) {
+            final JobServiceContext jc = mActiveServices.get(i);
+            final JobStatus js = jc.getRunningJobLocked();
+            if (js != null && js.matches(uid, jobId) && js.getSourcePackageName().equals(pkgName)) {
+                return jc.getEstimatedNetworkBytes();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the transferred network bytes if the job is running. Returns {@code null} if the job
+     * isn't running.
+     */
+    @Nullable
+    @GuardedBy("mLock")
+    Pair<Long, Long> getTransferredNetworkBytesLocked(String pkgName, int uid, int jobId) {
+        for (int i = 0; i < mActiveServices.size(); i++) {
+            final JobServiceContext jc = mActiveServices.get(i);
+            final JobStatus js = jc.getRunningJobLocked();
+            if (js != null && js.matches(uid, jobId) && js.getSourcePackageName().equals(pkgName)) {
+                return jc.getTransferredNetworkBytes();
+            }
+        }
+        return null;
+    }
+
     @NonNull
     private JobServiceContext createNewJobServiceContext() {
         return mInjector.createJobServiceContext(mService, this, mNotificationCoordinator,
@@ -2566,11 +2615,11 @@
     @VisibleForTesting
     static final class AssignmentInfo {
         public long minPreferredUidOnlyWaitingTimeMs;
-        public int numRunningTopEj;
+        public int numRunningImmediacyPrivileged;
 
         void clear() {
             minPreferredUidOnlyWaitingTimeMs = 0;
-            numRunningTopEj = 0;
+            numRunningImmediacyPrivileged = 0;
         }
     }
 
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 535f8d4..e9b9660 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -86,6 +86,7 @@
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -2850,8 +2851,8 @@
                 }
 
                 final boolean shouldForceBatchJob;
-                if (job.shouldTreatAsExpeditedJob()) {
-                    // Never batch expedited jobs, even for RESTRICTED apps.
+                if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiated()) {
+                    // Never batch expedited or user-initiated jobs, even for RESTRICTED apps.
                     shouldForceBatchJob = false;
                 } else if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) {
                     // Restricted jobs must always be batched
@@ -4168,6 +4169,100 @@
         }
     }
 
+    int getEstimatedNetworkBytes(PrintWriter pw, String pkgName, int userId, int jobId,
+            int byteOption) {
+        try {
+            final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
+                    userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
+            if (uid < 0) {
+                pw.print("unknown(");
+                pw.print(pkgName);
+                pw.println(")");
+                return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
+            }
+
+            synchronized (mLock) {
+                final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+                if (DEBUG) {
+                    Slog.d(TAG, "get-estimated-network-bytes " + uid + "/" + jobId + ": " + js);
+                }
+                if (js == null) {
+                    pw.print("unknown("); UserHandle.formatUid(pw, uid);
+                    pw.print("/jid"); pw.print(jobId); pw.println(")");
+                    return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
+                }
+
+                final long downloadBytes;
+                final long uploadBytes;
+                final Pair<Long, Long> bytes =
+                        mConcurrencyManager.getEstimatedNetworkBytesLocked(pkgName, uid, jobId);
+                if (bytes == null) {
+                    downloadBytes = js.getEstimatedNetworkDownloadBytes();
+                    uploadBytes = js.getEstimatedNetworkUploadBytes();
+                } else {
+                    downloadBytes = bytes.first;
+                    uploadBytes = bytes.second;
+                }
+                if (byteOption == JobSchedulerShellCommand.BYTE_OPTION_DOWNLOAD) {
+                    pw.println(downloadBytes);
+                } else {
+                    pw.println(uploadBytes);
+                }
+                pw.println();
+            }
+        } catch (RemoteException e) {
+            // can't happen
+        }
+        return 0;
+    }
+
+    int getTransferredNetworkBytes(PrintWriter pw, String pkgName, int userId, int jobId,
+            int byteOption) {
+        try {
+            final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
+                    userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
+            if (uid < 0) {
+                pw.print("unknown(");
+                pw.print(pkgName);
+                pw.println(")");
+                return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
+            }
+
+            synchronized (mLock) {
+                final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+                if (DEBUG) {
+                    Slog.d(TAG, "get-transferred-network-bytes " + uid + "/" + jobId + ": " + js);
+                }
+                if (js == null) {
+                    pw.print("unknown("); UserHandle.formatUid(pw, uid);
+                    pw.print("/jid"); pw.print(jobId); pw.println(")");
+                    return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
+                }
+
+                final long downloadBytes;
+                final long uploadBytes;
+                final Pair<Long, Long> bytes =
+                        mConcurrencyManager.getTransferredNetworkBytesLocked(pkgName, uid, jobId);
+                if (bytes == null) {
+                    downloadBytes = 0;
+                    uploadBytes = 0;
+                } else {
+                    downloadBytes = bytes.first;
+                    uploadBytes = bytes.second;
+                }
+                if (byteOption == JobSchedulerShellCommand.BYTE_OPTION_DOWNLOAD) {
+                    pw.println(downloadBytes);
+                } else {
+                    pw.println(uploadBytes);
+                }
+                pw.println();
+            }
+        } catch (RemoteException e) {
+            // can't happen
+        }
+        return 0;
+    }
+
     private boolean checkRunLongJobsPermission(int packageUid, String packageName) {
         // Returns true if both the appop and permission are granted.
         return PermissionChecker.checkPermissionForPreflight(getTestableContext(),
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
index 27268d2..36ba8dd 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -32,6 +32,9 @@
     public static final int CMD_ERR_NO_JOB = -1001;
     public static final int CMD_ERR_CONSTRAINTS = -1002;
 
+    static final int BYTE_OPTION_DOWNLOAD = 0;
+    static final int BYTE_OPTION_UPLOAD = 1;
+
     JobSchedulerService mInternal;
     IPackageManager mPM;
 
@@ -59,10 +62,18 @@
                     return getBatteryCharging(pw);
                 case "get-battery-not-low":
                     return getBatteryNotLow(pw);
+                case "get-estimated-download-bytes":
+                    return getEstimatedNetworkBytes(pw, BYTE_OPTION_DOWNLOAD);
+                case "get-estimated-upload-bytes":
+                    return getEstimatedNetworkBytes(pw, BYTE_OPTION_UPLOAD);
                 case "get-storage-seq":
                     return getStorageSeq(pw);
                 case "get-storage-not-low":
                     return getStorageNotLow(pw);
+                case "get-transferred-download-bytes":
+                    return getTransferredNetworkBytes(pw, BYTE_OPTION_DOWNLOAD);
+                case "get-transferred-upload-bytes":
+                    return getTransferredNetworkBytes(pw, BYTE_OPTION_UPLOAD);
                 case "get-job-state":
                     return getJobState(pw);
                 case "heartbeat":
@@ -304,6 +315,43 @@
         return 0;
     }
 
+    private int getEstimatedNetworkBytes(PrintWriter pw, int byteOption) throws Exception {
+        checkPermission("get estimated bytes");
+
+        int userId = UserHandle.USER_SYSTEM;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-u":
+                case "--user":
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                    break;
+
+                default:
+                    pw.println("Error: unknown option '" + opt + "'");
+                    return -1;
+            }
+        }
+
+        if (userId == UserHandle.USER_CURRENT) {
+            userId = ActivityManager.getCurrentUser();
+        }
+
+        final String pkgName = getNextArgRequired();
+        final String jobIdStr = getNextArgRequired();
+        final int jobId = Integer.parseInt(jobIdStr);
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            int ret = mInternal.getEstimatedNetworkBytes(pw, pkgName, userId, jobId, byteOption);
+            printError(ret, pkgName, userId, jobId);
+            return ret;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     private int getStorageSeq(PrintWriter pw) {
         int seq = mInternal.getStorageSeq();
         pw.println(seq);
@@ -316,8 +364,45 @@
         return 0;
     }
 
+    private int getTransferredNetworkBytes(PrintWriter pw, int byteOption) throws Exception {
+        checkPermission("get transferred bytes");
+
+        int userId = UserHandle.USER_SYSTEM;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-u":
+                case "--user":
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                    break;
+
+                default:
+                    pw.println("Error: unknown option '" + opt + "'");
+                    return -1;
+            }
+        }
+
+        if (userId == UserHandle.USER_CURRENT) {
+            userId = ActivityManager.getCurrentUser();
+        }
+
+        final String pkgName = getNextArgRequired();
+        final String jobIdStr = getNextArgRequired();
+        final int jobId = Integer.parseInt(jobIdStr);
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            int ret = mInternal.getTransferredNetworkBytes(pw, pkgName, userId, jobId, byteOption);
+            printError(ret, pkgName, userId, jobId);
+            return ret;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     private int getJobState(PrintWriter pw) throws Exception {
-        checkPermission("force timeout jobs");
+        checkPermission("get job state");
 
         int userId = UserHandle.USER_SYSTEM;
 
@@ -473,10 +558,30 @@
         pw.println("    Return whether the battery is currently considered to be charging.");
         pw.println("  get-battery-not-low");
         pw.println("    Return whether the battery is currently considered to not be low.");
+        pw.println("  get-estimated-download-bytes [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("    Return the most recent estimated download bytes for the job.");
+        pw.println("    Options:");
+        pw.println("      -u or --user: specify which user's job is to be run; the default is");
+        pw.println("         the primary or system user");
+        pw.println("  get-estimated-upload-bytes [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("    Return the most recent estimated upload bytes for the job.");
+        pw.println("    Options:");
+        pw.println("      -u or --user: specify which user's job is to be run; the default is");
+        pw.println("         the primary or system user");
         pw.println("  get-storage-seq");
         pw.println("    Return the last storage update sequence number that was received.");
         pw.println("  get-storage-not-low");
         pw.println("    Return whether storage is currently considered to not be low.");
+        pw.println("  get-transferred-download-bytes [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("    Return the most recent transferred download bytes for the job.");
+        pw.println("    Options:");
+        pw.println("      -u or --user: specify which user's job is to be run; the default is");
+        pw.println("         the primary or system user");
+        pw.println("  get-transferred-upload-bytes [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("    Return the most recent transferred upload bytes for the job.");
+        pw.println("    Options:");
+        pw.println("      -u or --user: specify which user's job is to be run; the default is");
+        pw.println("         the primary or system user");
         pw.println("  get-job-state [-u | --user USER_ID] PACKAGE JOB_ID");
         pw.println("    Return the current state of a job, may be any combination of:");
         pw.println("      pending: currently on the pending list, waiting to be active");
@@ -493,5 +598,4 @@
         pw.println("    Trigger wireless charging dock state.  Active by default.");
         pw.println();
     }
-
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index df47f17..0dcfd24 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -49,6 +49,7 @@
 import android.os.UserHandle;
 import android.util.EventLog;
 import android.util.IndentingPrintWriter;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.TimeUtils;
 
@@ -171,6 +172,11 @@
     /** The absolute maximum amount of time the job can run */
     private long mMaxExecutionTimeMillis;
 
+    private long mEstimatedDownloadBytes;
+    private long mEstimatedUploadBytes;
+    private long mTransferredDownloadBytes;
+    private long mTransferredUploadBytes;
+
     /**
      * The stop reason for a pending cancel. If there's not pending cancel, then the value should be
      * {@link JobParameters#STOP_REASON_UNDEFINED}.
@@ -306,6 +312,9 @@
             mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job);
             mMaxExecutionTimeMillis =
                     Math.max(mService.getMaxJobExecutionTimeMs(job), mMinExecutionGuaranteeMillis);
+            mEstimatedDownloadBytes = job.getEstimatedNetworkDownloadBytes();
+            mEstimatedUploadBytes = job.getEstimatedNetworkUploadBytes();
+            mTransferredDownloadBytes = mTransferredUploadBytes = 0;
 
             final long whenDeferred = job.getWhenStandbyDeferred();
             if (whenDeferred > 0) {
@@ -524,6 +533,16 @@
         return false;
     }
 
+    @GuardedBy("mLock")
+    Pair<Long, Long> getEstimatedNetworkBytes() {
+        return Pair.create(mEstimatedDownloadBytes, mEstimatedUploadBytes);
+    }
+
+    @GuardedBy("mLock")
+    Pair<Long, Long> getTransferredNetworkBytes() {
+        return Pair.create(mTransferredDownloadBytes, mTransferredUploadBytes);
+    }
+
     void doJobFinished(JobCallback cb, int jobId, boolean reschedule) {
         final long ident = Binder.clearCallingIdentity();
         try {
@@ -541,14 +560,26 @@
         }
     }
 
-    private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback jobCallback, int jobId,
+    private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback cb, int jobId,
             int workId, @BytesLong long transferredBytes) {
         // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+        synchronized (mLock) {
+            if (!verifyCallerLocked(cb)) {
+                return;
+            }
+            mTransferredDownloadBytes = transferredBytes;
+        }
     }
 
-    private void doAcknowledgeGetTransferredUploadBytesMessage(JobCallback jobCallback, int jobId,
+    private void doAcknowledgeGetTransferredUploadBytesMessage(JobCallback cb, int jobId,
             int workId, @BytesLong long transferredBytes) {
         // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+        synchronized (mLock) {
+            if (!verifyCallerLocked(cb)) {
+                return;
+            }
+            mTransferredUploadBytes = transferredBytes;
+        }
     }
 
     void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) {
@@ -603,6 +634,30 @@
         }
     }
 
+    private void doUpdateEstimatedNetworkBytes(JobCallback cb, int jobId,
+            @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+        // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+        synchronized (mLock) {
+            if (!verifyCallerLocked(cb)) {
+                return;
+            }
+            mEstimatedDownloadBytes = downloadBytes;
+            mEstimatedUploadBytes = uploadBytes;
+        }
+    }
+
+    private void doUpdateTransferredNetworkBytes(JobCallback cb, int jobId,
+            @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+        // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+        synchronized (mLock) {
+            if (!verifyCallerLocked(cb)) {
+                return;
+            }
+            mTransferredDownloadBytes = downloadBytes;
+            mTransferredUploadBytes = uploadBytes;
+        }
+    }
+
     private void doSetNotification(JobCallback cb, int jodId, int notificationId,
             Notification notification, int jobEndNotificationPolicy) {
         final int callingPid = Binder.getCallingPid();
@@ -627,16 +682,6 @@
         }
     }
 
-    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
-    }
-
-    private void doUpdateEstimatedNetworkBytes(JobCallback jobCallback, int jobId,
-            @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
-        // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
-    }
-
     /**
      * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
      * we intend to send to the client - we stop sending work when the service is unbound so until
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index 6166921..3610b0a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -149,11 +149,13 @@
             //   2. Waiting connectivity jobs would be ready with connectivity
             //   3. An existing network satisfies a waiting connectivity job's requirements
             //   4. TOP proc state
-            //   5. Existence of treat-as-EJ EJs (not just requested EJs)
-            //   6. FGS proc state
-            //   7. EJ enqueue time
-            //   8. Any other important job priorities/proc states
-            //   9. Enqueue time
+            //   5. Existence of treat-as-UI UIJs (not just requested UIJs)
+            //   6. Existence of treat-as-EJ EJs (not just requested EJs)
+            //   7. FGS proc state
+            //   8. UIJ enqueue time
+            //   9. EJ enqueue time
+            //   10. Any other important job priorities/proc states
+            //   11. Enqueue time
             // TODO: maybe consider number of jobs
             // TODO: consider IMPORTANT_WHILE_FOREGROUND bit
             final int runningPriority = prioritizeExistenceOver(0,
@@ -181,8 +183,13 @@
             if (topPriority != 0) {
                 return topPriority;
             }
-            // They're either both TOP or both not TOP. Prioritize the app that has runnable EJs
+            // They're either both TOP or both not TOP. Prioritize the app that has runnable UIJs
             // pending.
+            final int uijPriority = prioritizeExistenceOver(0, us1.numUIJs, us2.numUIJs);
+            if (uijPriority != 0) {
+                return uijPriority;
+            }
+            // Still equivalent. Prioritize the app that has runnable EJs pending.
             final int ejPriority = prioritizeExistenceOver(0, us1.numEJs, us2.numEJs);
             if (ejPriority != 0) {
                 return ejPriority;
@@ -195,6 +202,12 @@
             if (fgsPriority != 0) {
                 return fgsPriority;
             }
+            // Order them by UIJ enqueue time to help provide low UIJ latency.
+            if (us1.earliestUIJEnqueueTime < us2.earliestUIJEnqueueTime) {
+                return -1;
+            } else if (us1.earliestUIJEnqueueTime > us2.earliestUIJEnqueueTime) {
+                return 1;
+            }
             // Order them by EJ enqueue time to help provide low EJ latency.
             if (us1.earliestEJEnqueueTime < us2.earliestEJEnqueueTime) {
                 return -1;
@@ -414,7 +427,7 @@
         final UidStats uidStats =
                 getUidStats(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), true);
 
-        if (jobStatus.shouldTreatAsExpeditedJob()) {
+        if (jobStatus.shouldTreatAsExpeditedJob() && jobStatus.shouldTreatAsUserInitiated()) {
             if (!jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)) {
                 // Don't request a direct hole through any of the firewalls. Instead, mark the
                 // constraint as satisfied if the network is available, and the job will get
@@ -936,10 +949,12 @@
             if (us.lastUpdatedElapsed + MIN_STATS_UPDATE_INTERVAL_MS < nowElapsed) {
                 us.earliestEnqueueTime = Long.MAX_VALUE;
                 us.earliestEJEnqueueTime = Long.MAX_VALUE;
+                us.earliestUIJEnqueueTime = Long.MAX_VALUE;
                 us.numReadyWithConnectivity = 0;
                 us.numRequestedNetworkAvailable = 0;
                 us.numRegular = 0;
                 us.numEJs = 0;
+                us.numUIJs = 0;
 
                 for (int j = 0; j < jobs.size(); ++j) {
                     JobStatus job = jobs.valueAt(j);
@@ -956,10 +971,15 @@
                         if (job.shouldTreatAsExpeditedJob() || job.startedAsExpeditedJob) {
                             us.earliestEJEnqueueTime =
                                     Math.min(us.earliestEJEnqueueTime, job.enqueueTime);
+                        } else if (job.shouldTreatAsUserInitiated()) {
+                            us.earliestUIJEnqueueTime =
+                                    Math.min(us.earliestUIJEnqueueTime, job.enqueueTime);
                         }
                     }
                     if (job.shouldTreatAsExpeditedJob() || job.startedAsExpeditedJob) {
                         us.numEJs++;
+                    } else if (job.shouldTreatAsUserInitiated()) {
+                        us.numUIJs++;
                     } else {
                         us.numRegular++;
                     }
@@ -1466,8 +1486,10 @@
         public int numRequestedNetworkAvailable;
         public int numEJs;
         public int numRegular;
+        public int numUIJs;
         public long earliestEnqueueTime;
         public long earliestEJEnqueueTime;
+        public long earliestUIJEnqueueTime;
         public long lastUpdatedElapsed;
 
         private UidStats(int uid) {
@@ -1485,6 +1507,7 @@
             pw.print("#reg", numRegular);
             pw.print("earliestEnqueue", earliestEnqueueTime);
             pw.print("earliestEJEnqueue", earliestEJEnqueueTime);
+            pw.print("earliestUIJEnqueue", earliestUIJEnqueueTime);
             pw.print("updated=");
             TimeUtils.formatDuration(lastUpdatedElapsed - nowElapsed, pw);
             pw.println("}");
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 419127e..251a4da 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
@@ -388,6 +388,8 @@
      */
     public boolean startedAsExpeditedJob = false;
 
+    public boolean startedWithImmediacyPrivilege = false;
+
     // If non-null, this is work that has been enqueued for the job.
     public ArrayList<JobWorkItem> pendingWork;
 
@@ -1369,9 +1371,7 @@
      * @return true if this is a job whose execution should be made visible to the user.
      */
     public boolean isUserVisibleJob() {
-        // TODO(255767350): limit to user-initiated jobs
-        // Placeholder implementation until we have the code in
-        return shouldTreatAsExpeditedJob();
+        return shouldTreatAsUserInitiated();
     }
 
     /**
@@ -1382,12 +1382,14 @@
         return appHasDozeExemption
                 || (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0
                 || ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob)
+                || shouldTreatAsUserInitiated()
                 && (mDynamicConstraints & CONSTRAINT_DEVICE_NOT_DOZING) == 0);
     }
 
     boolean canRunInBatterySaver() {
         return (getInternalFlags() & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0
                 || ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob)
+                || shouldTreatAsUserInitiated()
                 && (mDynamicConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) == 0);
     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 1a775b4..6869936 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -2187,8 +2187,10 @@
                 }
                 // component-level enable/disable can affect bucketing, so we always
                 // reevaluate that for any PACKAGE_CHANGED
-                mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkgName)
-                    .sendToTarget();
+                if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+                    mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkgName)
+                            .sendToTarget();
+                }
             }
             if ((Intent.ACTION_PACKAGE_REMOVED.equals(action) ||
                     Intent.ACTION_PACKAGE_ADDED.equals(action))) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 1ba99f9..39ac0d2e 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4150,6 +4150,8 @@
     method @Deprecated public android.app.FragmentManager getFragmentManager();
     method public android.content.Intent getIntent();
     method @Nullable public Object getLastNonConfigurationInstance();
+    method @Nullable public String getLaunchedFromPackage();
+    method public int getLaunchedFromUid();
     method @NonNull public android.view.LayoutInflater getLayoutInflater();
     method @Deprecated public android.app.LoaderManager getLoaderManager();
     method @NonNull public String getLocalClassName();
@@ -4292,6 +4294,7 @@
     method @Deprecated public final void removeDialog(int);
     method public void reportFullyDrawn();
     method public android.view.DragAndDropPermissions requestDragAndDropPermissions(android.view.DragEvent);
+    method public void requestFullscreenMode(@NonNull int, @Nullable android.os.OutcomeReceiver<java.lang.Void,java.lang.Throwable>);
     method public final void requestPermissions(@NonNull String[], int);
     method public final void requestShowKeyboardShortcuts();
     method @Deprecated public boolean requestVisibleBehind(boolean);
@@ -4378,6 +4381,8 @@
     field public static final int DEFAULT_KEYS_SEARCH_LOCAL = 3; // 0x3
     field public static final int DEFAULT_KEYS_SHORTCUT = 2; // 0x2
     field protected static final int[] FOCUSED_STATE_SET;
+    field public static final int FULLSCREEN_MODE_REQUEST_ENTER = 1; // 0x1
+    field public static final int FULLSCREEN_MODE_REQUEST_EXIT = 0; // 0x0
     field public static final int RESULT_CANCELED = 0; // 0x0
     field public static final int RESULT_FIRST_USER = 1; // 0x1
     field public static final int RESULT_OK = -1; // 0xffffffff
@@ -4619,6 +4624,7 @@
     method public android.app.ActivityOptions setLaunchDisplayId(int);
     method public android.app.ActivityOptions setLockTaskEnabled(boolean);
     method public void setPendingIntentBackgroundActivityLaunchAllowed(boolean);
+    method @NonNull public android.app.ActivityOptions setShareIdentityEnabled(boolean);
     method @NonNull public android.app.ActivityOptions setSplashScreenStyle(int);
     method public android.os.Bundle toBundle();
     method public void update(android.app.ActivityOptions);
@@ -10589,6 +10595,7 @@
     field public static final String EXTRA_MIME_TYPES = "android.intent.extra.MIME_TYPES";
     field public static final String EXTRA_NOT_UNKNOWN_SOURCE = "android.intent.extra.NOT_UNKNOWN_SOURCE";
     field public static final String EXTRA_ORIGINATING_URI = "android.intent.extra.ORIGINATING_URI";
+    field public static final String EXTRA_PACKAGES = "android.intent.extra.PACKAGES";
     field public static final String EXTRA_PACKAGE_NAME = "android.intent.extra.PACKAGE_NAME";
     field public static final String EXTRA_PERMISSION_GROUP_NAME = "android.intent.extra.PERMISSION_GROUP_NAME";
     field public static final String EXTRA_PHONE_NUMBER = "android.intent.extra.PHONE_NUMBER";
@@ -11718,9 +11725,12 @@
     method public void unregisterSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback);
     method public void updateSessionAppIcon(int, @Nullable android.graphics.Bitmap);
     method public void updateSessionAppLabel(int, @Nullable CharSequence);
+    method public void waitForInstallConstraints(@NonNull java.util.List<java.lang.String>, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull android.content.IntentSender, long);
     field public static final String ACTION_SESSION_COMMITTED = "android.content.pm.action.SESSION_COMMITTED";
     field public static final String ACTION_SESSION_DETAILS = "android.content.pm.action.SESSION_DETAILS";
     field public static final String ACTION_SESSION_UPDATED = "android.content.pm.action.SESSION_UPDATED";
+    field public static final String EXTRA_INSTALL_CONSTRAINTS = "android.content.pm.extra.INSTALL_CONSTRAINTS";
+    field public static final String EXTRA_INSTALL_CONSTRAINTS_RESULT = "android.content.pm.extra.INSTALL_CONSTRAINTS_RESULT";
     field public static final String EXTRA_OTHER_PACKAGE_NAME = "android.content.pm.extra.OTHER_PACKAGE_NAME";
     field public static final String EXTRA_PACKAGE_NAME = "android.content.pm.extra.PACKAGE_NAME";
     field public static final String EXTRA_PRE_APPROVAL = "android.content.pm.extra.PRE_APPROVAL";
@@ -12175,6 +12185,7 @@
     field public static final String FEATURE_RAM_NORMAL = "android.hardware.ram.normal";
     field public static final String FEATURE_SCREEN_LANDSCAPE = "android.hardware.screen.landscape";
     field public static final String FEATURE_SCREEN_PORTRAIT = "android.hardware.screen.portrait";
+    field public static final String FEATURE_SEAMLESS_REFRESH_RATE_SWITCHING = "android.software.seamless_refresh_rate_switching";
     field public static final String FEATURE_SECURELY_REMOVES_USERS = "android.software.securely_removes_users";
     field public static final String FEATURE_SECURE_LOCK_SCREEN = "android.software.secure_lock_screen";
     field public static final String FEATURE_SECURITY_MODEL_COMPATIBLE = "android.hardware.security.model.compatible";
@@ -17936,6 +17947,7 @@
     method public int capture(@NonNull android.hardware.camera2.CaptureRequest, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback) throws android.hardware.camera2.CameraAccessException;
     method public void close() throws android.hardware.camera2.CameraAccessException;
     method @NonNull public android.hardware.camera2.CameraDevice getDevice();
+    method @Nullable public android.util.Pair<java.lang.Long,java.lang.Long> getRealtimeStillCaptureLatency() throws android.hardware.camera2.CameraAccessException;
     method public int setRepeatingRequest(@NonNull android.hardware.camera2.CaptureRequest, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback) throws android.hardware.camera2.CameraAccessException;
     method public void stopRepeating() throws android.hardware.camera2.CameraAccessException;
   }
@@ -20518,6 +20530,7 @@
     field public static final int ENCODING_DOLBY_MAT = 19; // 0x13
     field public static final int ENCODING_DOLBY_TRUEHD = 14; // 0xe
     field public static final int ENCODING_DRA = 28; // 0x1c
+    field public static final int ENCODING_DSD = 31; // 0x1f
     field public static final int ENCODING_DTS = 7; // 0x7
     field public static final int ENCODING_DTS_HD = 8; // 0x8
     field public static final int ENCODING_DTS_HD_MA = 29; // 0x1d
@@ -20911,6 +20924,7 @@
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field public static final int AUDIO_ENCAPSULATION_TYPE_IEC61937 = 1; // 0x1
     field public static final int AUDIO_ENCAPSULATION_TYPE_NONE = 0; // 0x0
+    field public static final int AUDIO_ENCAPSULATION_TYPE_PCM = 2; // 0x2
     field @NonNull public static final android.os.Parcelable.Creator<android.media.AudioProfile> CREATOR;
   }
 
@@ -25452,11 +25466,21 @@
 
   public abstract static class MediaProjection.Callback {
     ctor public MediaProjection.Callback();
+    method public void onCapturedContentResize(int, int);
     method public void onStop();
   }
 
+  public final class MediaProjectionConfig implements android.os.Parcelable {
+    method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForDisplay(@IntRange(from=android.view.Display.DEFAULT_DISPLAY, to=android.view.Display.DEFAULT_DISPLAY) int);
+    method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForUserChoice();
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.media.projection.MediaProjectionConfig> CREATOR;
+  }
+
   public final class MediaProjectionManager {
-    method public android.content.Intent createScreenCaptureIntent();
+    method @NonNull public android.content.Intent createScreenCaptureIntent();
+    method @NonNull public android.content.Intent createScreenCaptureIntent(@NonNull android.media.projection.MediaProjectionConfig);
     method public android.media.projection.MediaProjection getMediaProjection(int, @NonNull android.content.Intent);
   }
 
@@ -26785,6 +26809,7 @@
     method public boolean onKeyUp(int, @NonNull android.view.KeyEvent);
     method public void onMediaViewSizeChanged(@Px int, @Px int);
     method public void onRecordingStarted(@NonNull String);
+    method public void onRecordingStopped(@NonNull String);
     method public abstract void onRelease();
     method public void onResetInteractiveApp();
     method public abstract boolean onSetSurface(@Nullable android.view.Surface);
@@ -26811,6 +26836,7 @@
     method @CallSuper public void requestCurrentTvInputId();
     method @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
     method @CallSuper public void requestStartRecording(@Nullable android.net.Uri);
+    method @CallSuper public void requestStopRecording(@NonNull String);
     method @CallSuper public void requestStreamVolume();
     method @CallSuper public void requestTrackInfoList();
     method @CallSuper public void sendPlaybackCommandRequest(@NonNull String, @Nullable android.os.Bundle);
@@ -26843,6 +26869,7 @@
     method @Nullable public android.media.tv.interactive.TvInteractiveAppView.OnUnhandledInputEventListener getOnUnhandledInputEventListener();
     method public void notifyError(@NonNull String, @NonNull android.os.Bundle);
     method public void notifyRecordingStarted(@NonNull String);
+    method public void notifyRecordingStopped(@NonNull String);
     method public void onAttachedToWindow();
     method public void onDetachedFromWindow();
     method public void onLayout(boolean, int, int, int, int);
@@ -26885,6 +26912,7 @@
     method public void onRequestCurrentTvInputId(@NonNull String);
     method public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
     method public void onRequestStartRecording(@NonNull String, @Nullable android.net.Uri);
+    method public void onRequestStopRecording(@NonNull String, @NonNull String);
     method public void onRequestStreamVolume(@NonNull String);
     method public void onRequestTrackInfoList(@NonNull String);
     method public void onSetVideoBounds(@NonNull String, @NonNull android.graphics.Rect);
@@ -27851,6 +27879,7 @@
   public final class VcnConfig implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public java.util.Set<android.net.vcn.VcnGatewayConnectionConfig> getGatewayConnectionConfigs();
+    method @NonNull public java.util.Set<java.lang.Integer> getRestrictedUnderlyingNetworkTransports();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.vcn.VcnConfig> CREATOR;
   }
@@ -27859,6 +27888,7 @@
     ctor public VcnConfig.Builder(@NonNull android.content.Context);
     method @NonNull public android.net.vcn.VcnConfig.Builder addGatewayConnectionConfig(@NonNull android.net.vcn.VcnGatewayConnectionConfig);
     method @NonNull public android.net.vcn.VcnConfig build();
+    method @NonNull public android.net.vcn.VcnConfig.Builder setRestrictedUnderlyingNetworkTransports(@NonNull java.util.Set<java.lang.Integer>);
   }
 
   public final class VcnGatewayConnectionConfig {
@@ -27867,13 +27897,17 @@
     method @IntRange(from=0x500) public int getMaxMtu();
     method @NonNull public long[] getRetryIntervalsMillis();
     method @NonNull public java.util.List<android.net.vcn.VcnUnderlyingNetworkTemplate> getVcnUnderlyingNetworkPriorities();
+    method public boolean hasGatewayOption(int);
+    field public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0; // 0x0
   }
 
   public static final class VcnGatewayConnectionConfig.Builder {
     ctor public VcnGatewayConnectionConfig.Builder(@NonNull String, @NonNull android.net.ipsec.ike.IkeTunnelConnectionParams);
     method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addExposedCapability(int);
+    method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addGatewayOption(int);
     method @NonNull public android.net.vcn.VcnGatewayConnectionConfig build();
     method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeExposedCapability(int);
+    method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeGatewayOption(int);
     method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setMaxMtu(@IntRange(from=0x500) int);
     method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setRetryIntervalsMillis(@NonNull long[]);
     method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setVcnUnderlyingNetworkPriorities(@NonNull java.util.List<android.net.vcn.VcnUnderlyingNetworkTemplate>);
@@ -35114,6 +35148,7 @@
     field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/phone_v2";
     field public static final android.net.Uri CONTENT_URI;
     field public static final android.net.Uri ENTERPRISE_CONTENT_FILTER_URI;
+    field @NonNull public static final android.net.Uri ENTERPRISE_CONTENT_URI;
     field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES";
@@ -35294,6 +35329,7 @@
     field public static final String CONTENT_VCARD_TYPE = "text/x-vcard";
     field public static final android.net.Uri CONTENT_VCARD_URI;
     field public static final android.net.Uri ENTERPRISE_CONTENT_FILTER_URI;
+    field @NonNull public static final android.net.Uri ENTERPRISE_CONTENT_URI;
     field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS";
     field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES";
@@ -36267,7 +36303,7 @@
     field public static final String PARENTAL_CONTROL_LAST_UPDATE = "parental_control_last_update";
     field public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url";
     field public static final String RTT_CALLING_MODE = "rtt_calling_mode";
-    field public static final String SECURE_FRP_MODE = "secure_frp_mode";
+    field @Deprecated public static final String SECURE_FRP_MODE = "secure_frp_mode";
     field public static final String SELECTED_INPUT_METHOD_SUBTYPE = "selected_input_method_subtype";
     field public static final String SETTINGS_CLASSNAME = "settings_classname";
     field public static final String SKIP_FIRST_USE_HINTS = "skip_first_use_hints";
@@ -38263,7 +38299,7 @@
     ctor public AlreadyPersonalizedException(@NonNull String, @NonNull Throwable);
   }
 
-  public class AuthenticationKeyMetadata {
+  public final class AuthenticationKeyMetadata {
     method @NonNull public java.time.Instant getExpirationDate();
     method @IntRange(from=0) public int getUsageCount();
   }
@@ -42017,11 +42053,15 @@
   }
 
   public class CarrierConfigManager {
-    method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfig();
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfig();
+    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public android.os.PersistableBundle getConfig(@NonNull java.lang.String...);
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfigByComponentForSubId(@NonNull String, int);
-    method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfigForSubId(int);
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfigForSubId(int);
+    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public android.os.PersistableBundle getConfigForSubId(int, @NonNull java.lang.String...);
     method public static boolean isConfigForIdentifiedCarrier(android.os.PersistableBundle);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyConfigChangedForSubId(int);
+    method public void registerCarrierConfigChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.CarrierConfigManager.CarrierConfigChangeListener);
+    method public void unregisterCarrierConfigChangeListener(@NonNull android.telephony.CarrierConfigManager.CarrierConfigChangeListener);
     field public static final String ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED";
     field public static final int CARRIER_NR_AVAILABILITY_NSA = 1; // 0x1
     field public static final int CARRIER_NR_AVAILABILITY_SA = 2; // 0x2
@@ -42351,6 +42391,10 @@
     field public static final String KEY_PREFIX = "bsf.";
   }
 
+  public static interface CarrierConfigManager.CarrierConfigChangeListener {
+    method public void onCarrierConfigChanged(int, int, int, int);
+  }
+
   public static final class CarrierConfigManager.Gps {
     field public static final String KEY_PERSIST_LPP_MODE_BOOL = "gps.persist_lpp_mode_bool";
     field public static final String KEY_PREFIX = "gps.";
@@ -49564,6 +49608,7 @@
     field public static final int KEYCODE_PROG_YELLOW = 185; // 0xb9
     field public static final int KEYCODE_Q = 45; // 0x2d
     field public static final int KEYCODE_R = 46; // 0x2e
+    field public static final int KEYCODE_RECENT_APPS = 312; // 0x138
     field public static final int KEYCODE_REFRESH = 285; // 0x11d
     field public static final int KEYCODE_RIGHT_BRACKET = 72; // 0x48
     field public static final int KEYCODE_RO = 217; // 0xd9
@@ -52762,12 +52807,14 @@
     method public void addAudioDescriptionRequestedChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.accessibility.AccessibilityManager.AudioDescriptionRequestedChangeListener);
     method public boolean addTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener);
     method public void addTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener, @Nullable android.os.Handler);
+    method public void addUiContrastChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.accessibility.AccessibilityManager.UiContrastChangeListener);
     method @ColorInt public int getAccessibilityFocusColor();
     method public int getAccessibilityFocusStrokeWidth();
     method @Deprecated public java.util.List<android.content.pm.ServiceInfo> getAccessibilityServiceList();
     method public java.util.List<android.accessibilityservice.AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int);
     method public java.util.List<android.accessibilityservice.AccessibilityServiceInfo> getInstalledAccessibilityServiceList();
     method public int getRecommendedTimeoutMillis(int, int);
+    method @FloatRange(from=-1.0F, to=1.0f) public float getUiContrast();
     method public void interrupt();
     method public static boolean isAccessibilityButtonSupported();
     method public boolean isAudioDescriptionRequested();
@@ -52779,6 +52826,7 @@
     method public boolean removeAccessibilityStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener);
     method public boolean removeAudioDescriptionRequestedChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AudioDescriptionRequestedChangeListener);
     method public boolean removeTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener);
+    method public void removeUiContrastChangeListener(@NonNull android.view.accessibility.AccessibilityManager.UiContrastChangeListener);
     method public void sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
     field public static final int FLAG_CONTENT_CONTROLS = 4; // 0x4
     field public static final int FLAG_CONTENT_ICONS = 1; // 0x1
@@ -52801,6 +52849,10 @@
     method public void onTouchExplorationStateChanged(boolean);
   }
 
+  public static interface AccessibilityManager.UiContrastChangeListener {
+    method public void onUiContrastChanged(@FloatRange(from=-1.0F, to=1.0f) float);
+  }
+
   public class AccessibilityNodeInfo implements android.os.Parcelable {
     ctor public AccessibilityNodeInfo();
     ctor public AccessibilityNodeInfo(@NonNull android.view.View);
@@ -54541,6 +54593,8 @@
     method @NonNull public android.text.SegmentFinder getGraphemeSegmentFinder();
     method @NonNull public android.text.SegmentFinder getLineSegmentFinder();
     method @NonNull public android.graphics.Matrix getMatrix();
+    method public int getOffsetForPosition(float, float);
+    method @Nullable public int[] getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy);
     method public int getStart();
     method @NonNull public android.text.SegmentFinder getWordSegmentFinder();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 98c78fe..314fd03 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -99,6 +99,10 @@
 
   public class ApplicationInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
     method @NonNull public java.util.List<android.content.pm.SharedLibraryInfo> getSharedLibraryInfos();
+    field public static final int HIDDEN_API_ENFORCEMENT_DEFAULT = -1; // 0xffffffff
+    field public static final int HIDDEN_API_ENFORCEMENT_DISABLED = 0; // 0x0
+    field public static final int HIDDEN_API_ENFORCEMENT_ENABLED = 2; // 0x2
+    field public static final int HIDDEN_API_ENFORCEMENT_JUST_WARN = 1; // 0x1
   }
 
   public abstract class PackageManager {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 6f341f2..fb74260 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -352,6 +352,7 @@
     field public static final String USE_COLORIZED_NOTIFICATIONS = "android.permission.USE_COLORIZED_NOTIFICATIONS";
     field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK";
     field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED";
+    field public static final String WAKEUP_SURFACE_FLINGER = "android.permission.WAKEUP_SURFACE_FLINGER";
     field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS";
     field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS";
     field public static final String WIFI_ACCESS_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS";
@@ -388,6 +389,7 @@
     field public static final int sdkVersion = 16844304; // 0x1010610
     field public static final int supportsAmbientMode = 16844173; // 0x101058d
     field public static final int userRestriction = 16844164; // 0x1010584
+    field public static final int visualQueryDetectionService;
   }
 
   public static final class R.bool {
@@ -2999,6 +3001,7 @@
     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.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
     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();
@@ -3319,7 +3322,6 @@
     field public static final String EXTRA_INSTANT_APP_TOKEN = "android.intent.extra.INSTANT_APP_TOKEN";
     field public static final String EXTRA_LONG_VERSION_CODE = "android.intent.extra.LONG_VERSION_CODE";
     field public static final String EXTRA_ORIGINATING_UID = "android.intent.extra.ORIGINATING_UID";
-    field public static final String EXTRA_PACKAGES = "android.intent.extra.PACKAGES";
     field public static final String EXTRA_PERMISSION_NAME = "android.intent.extra.PERMISSION_NAME";
     field public static final String EXTRA_REASON = "android.intent.extra.REASON";
     field public static final String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK";
@@ -3608,6 +3610,7 @@
   public abstract class PackageManager {
     method @RequiresPermission("android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS") public abstract void addOnPermissionsChangeListener(@NonNull android.content.pm.PackageManager.OnPermissionsChangedListener);
     method public abstract boolean arePermissionsIndividuallyControlled();
+    method @NonNull public boolean[] canPackageQuery(@NonNull String, @NonNull String[]) throws android.content.pm.PackageManager.NameNotFoundException;
     method @NonNull public abstract java.util.List<android.content.IntentFilter> getAllIntentFilters(@NonNull String);
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException;
     method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, @NonNull android.content.pm.PackageManager.ApplicationInfoFlags, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -4784,6 +4787,24 @@
     method @NonNull public android.hardware.input.VirtualMouseScrollEvent.Builder setYAxisMovement(@FloatRange(from=-1.0F, to=1.0f) float);
   }
 
+  public class VirtualNavigationTouchpad implements java.io.Closeable {
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
+  }
+
+  public final class VirtualNavigationTouchpadConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getHeight();
+    method public int getWidth();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualNavigationTouchpadConfig> CREATOR;
+  }
+
+  public static final class VirtualNavigationTouchpadConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualNavigationTouchpadConfig.Builder> {
+    ctor public VirtualNavigationTouchpadConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int);
+    method @NonNull public android.hardware.input.VirtualNavigationTouchpadConfig build();
+  }
+
   public final class VirtualTouchEvent implements android.os.Parcelable {
     method public int describeContents();
     method public int getAction();
@@ -5275,11 +5296,12 @@
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector> CREATOR;
     field public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; // 0x1
+    field public static final int IDENTIFIER_TYPE_DAB_DMB_SID_EXT = 14; // 0xe
     field public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6; // 0x6
     field public static final int IDENTIFIER_TYPE_DAB_FREQUENCY = 8; // 0x8
     field public static final int IDENTIFIER_TYPE_DAB_SCID = 7; // 0x7
-    field public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5
-    field public static final int IDENTIFIER_TYPE_DAB_SID_EXT = 5; // 0x5
+    field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5
+    field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SID_EXT = 5; // 0x5
     field public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; // 0xa
     field @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb
     field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9
@@ -5316,7 +5338,7 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector.Identifier> CREATOR;
   }
 
-  @IntDef(prefix={"IDENTIFIER_TYPE_"}, value={android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_INVALID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_RDS_PI, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_MODULATION, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL}) @IntRange(from=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.IdentifierType {
+  @IntDef(prefix={"IDENTIFIER_TYPE_"}, value={android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_INVALID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_RDS_PI, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_MODULATION, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT}) @IntRange(from=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.IdentifierType {
   }
 
   @Deprecated @IntDef(prefix={"PROGRAM_TYPE_"}, value={android.hardware.radio.ProgramSelector.PROGRAM_TYPE_INVALID, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_AM, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_FM, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_AM_HD, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_FM_HD, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_DAB, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_DRMO, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_SXM}) @IntRange(from=android.hardware.radio.ProgramSelector.PROGRAM_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.PROGRAM_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.ProgramType {
@@ -5531,30 +5553,31 @@
 
   public abstract class RadioTuner {
     ctor public RadioTuner();
-    method public abstract int cancel();
-    method public abstract void cancelAnnouncement();
-    method public abstract void close();
-    method @Deprecated public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]);
-    method @Nullable public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter);
-    method public abstract boolean getMute();
-    method @NonNull public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>);
-    method @Deprecated public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]);
-    method @Deprecated @NonNull public abstract java.util.List<android.hardware.radio.RadioManager.ProgramInfo> getProgramList(@Nullable java.util.Map<java.lang.String,java.lang.String>);
-    method public abstract boolean hasControl();
-    method @Deprecated public abstract boolean isAnalogForced();
-    method @Deprecated public abstract boolean isAntennaConnected();
-    method public boolean isConfigFlagSet(int);
-    method public boolean isConfigFlagSupported(int);
-    method public abstract int scan(int, boolean);
-    method @Deprecated public abstract void setAnalogForced(boolean);
-    method public void setConfigFlag(int, boolean);
-    method @Deprecated public abstract int setConfiguration(android.hardware.radio.RadioManager.BandConfig);
-    method public abstract int setMute(boolean);
-    method @NonNull public java.util.Map<java.lang.String,java.lang.String> setParameters(@NonNull java.util.Map<java.lang.String,java.lang.String>);
-    method public abstract boolean startBackgroundScan();
-    method public abstract int step(int, boolean);
-    method @Deprecated public abstract int tune(int, int);
-    method public abstract void tune(@NonNull android.hardware.radio.ProgramSelector);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int cancel();
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void cancelAnnouncement();
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void close();
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]);
+    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean getMute();
+    method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]);
+    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract java.util.List<android.hardware.radio.RadioManager.ProgramInfo> getProgramList(@Nullable java.util.Map<java.lang.String,java.lang.String>);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean hasControl();
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean isAnalogForced();
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean isAntennaConnected();
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public boolean isConfigFlagSet(int);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public boolean isConfigFlagSupported(int);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int scan(int, boolean);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public int seek(int, boolean);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void setAnalogForced(boolean);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public void setConfigFlag(int, boolean);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int setConfiguration(android.hardware.radio.RadioManager.BandConfig);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int setMute(boolean);
+    method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> setParameters(@NonNull java.util.Map<java.lang.String,java.lang.String>);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean startBackgroundScan();
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int step(int, boolean);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int tune(int, int);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void tune(@NonNull android.hardware.radio.ProgramSelector);
     field public static final int DIRECTION_DOWN = 1; // 0x1
     field public static final int DIRECTION_UP = 0; // 0x0
     field @Deprecated public static final int ERROR_BACKGROUND_SCAN_FAILED = 6; // 0x6
@@ -5564,6 +5587,14 @@
     field @Deprecated public static final int ERROR_HARDWARE_FAILURE = 0; // 0x0
     field @Deprecated public static final int ERROR_SCAN_TIMEOUT = 3; // 0x3
     field @Deprecated public static final int ERROR_SERVER_DIED = 1; // 0x1
+    field public static final int TUNER_RESULT_CANCELED = 6; // 0x6
+    field public static final int TUNER_RESULT_INTERNAL_ERROR = 1; // 0x1
+    field public static final int TUNER_RESULT_INVALID_ARGUMENTS = 2; // 0x2
+    field public static final int TUNER_RESULT_INVALID_STATE = 3; // 0x3
+    field public static final int TUNER_RESULT_NOT_SUPPORTED = 4; // 0x4
+    field public static final int TUNER_RESULT_OK = 0; // 0x0
+    field public static final int TUNER_RESULT_TIMEOUT = 5; // 0x5
+    field public static final int TUNER_RESULT_UNKNOWN_ERROR = 7; // 0x7
   }
 
   public abstract static class RadioTuner.Callback {
@@ -5571,6 +5602,7 @@
     method public void onAntennaState(boolean);
     method public void onBackgroundScanAvailabilityChange(boolean);
     method public void onBackgroundScanComplete();
+    method public void onConfigFlagUpdated(int, boolean);
     method @Deprecated public void onConfigurationChanged(android.hardware.radio.RadioManager.BandConfig);
     method public void onControlChanged(boolean);
     method public void onEmergencyAnnouncement(boolean);
@@ -10671,10 +10703,6 @@
     field public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
-  public static final class ContactsContract.CommonDataKinds.Phone implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
-    field @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public static final android.net.Uri ENTERPRISE_CONTENT_URI;
-  }
-
   @Deprecated public static final class ContactsContract.MetadataSync implements android.provider.BaseColumns android.provider.ContactsContract.MetadataSyncColumns {
     field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_metadata";
     field @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_metadata";
@@ -10883,6 +10911,7 @@
     field public static final String INSTALL_CARRIER_APP_NOTIFICATION_SLEEP_MILLIS = "install_carrier_app_notification_sleep_millis";
     field public static final String OTA_DISABLE_AUTOMATIC_UPDATE = "ota_disable_automatic_update";
     field public static final String REQUIRE_PASSWORD_TO_DECRYPT = "require_password_to_decrypt";
+    field public static final String SECURE_FRP_MODE = "secure_frp_mode";
     field public static final String TETHER_OFFLOAD_DISABLED = "tether_offload_disabled";
     field public static final String TETHER_SUPPORTED = "tether_supported";
     field public static final String THEATER_MODE_ON = "theater_mode_on";
@@ -15737,9 +15766,12 @@
     method public void onFeatureRemoved();
     method public boolean queryCapabilityConfiguration(int, int);
     method @NonNull public final android.telephony.ims.feature.MmTelFeature.MmTelCapabilities queryCapabilityStatus();
+    method public final void setCallAudioHandler(int);
     method public void setTerminalBasedCallWaitingStatus(boolean);
     method public void setUiTtyMode(int, @Nullable android.os.Message);
     method public int shouldProcessCall(@NonNull String[]);
+    field public static final int AUDIO_HANDLER_ANDROID = 0; // 0x0
+    field public static final int AUDIO_HANDLER_BASEBAND = 1; // 0x1
     field public static final String EXTRA_IS_UNKNOWN_CALL = "android.telephony.ims.feature.extra.IS_UNKNOWN_CALL";
     field public static final String EXTRA_IS_USSD = "android.telephony.ims.feature.extra.IS_USSD";
     field public static final int PROCESS_CALL_CSFB = 1; // 0x1
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 2820ef9..c36aa61 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2203,10 +2203,6 @@
     field public static final String HIDDEN_COLUMN_PREFIX = "x_";
   }
 
-  public static final class ContactsContract.CommonDataKinds.Phone implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
-    field @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public static final android.net.Uri ENTERPRISE_CONTENT_URI;
-  }
-
   public static final class ContactsContract.PinnedPositions {
     field public static final String UNDEMOTE_METHOD = "undemote";
   }
@@ -2930,7 +2926,7 @@
     method public static String actionToString(int);
     method public final void setDisplayId(int);
     field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800
-    field public static final int LAST_KEYCODE = 311; // 0x137
+    field public static final int LAST_KEYCODE = 312; // 0x138
   }
 
   public final class KeyboardShortcutGroup implements android.os.Parcelable {
@@ -2944,6 +2940,7 @@
   }
 
   public final class MotionEvent extends android.view.InputEvent implements android.os.Parcelable {
+    method public int getDisplayId();
     method public void setActionButton(int);
     method public void setButtonState(int);
     method public void setDisplayId(int);
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 30ff052..b9eb443 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -83,6 +83,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.OutcomeReceiver;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.os.Process;
@@ -987,6 +988,17 @@
     /** @hide */
     boolean mIsInPictureInPictureMode;
 
+    /** @hide */
+    @IntDef(prefix = { "FULLSCREEN_REQUEST_" }, value = {
+            FULLSCREEN_MODE_REQUEST_EXIT,
+            FULLSCREEN_MODE_REQUEST_ENTER
+    })
+    public @interface FullscreenModeRequest {}
+
+    public static final int FULLSCREEN_MODE_REQUEST_EXIT = 0;
+
+    public static final int FULLSCREEN_MODE_REQUEST_ENTER = 1;
+
     private boolean mShouldDockBigOverlays;
 
     private UiTranslationController mUiTranslationController;
@@ -3001,6 +3013,36 @@
     }
 
     /**
+     * Request to put the a freeform activity into fullscreen. This will only be allowed if the
+     * activity is on a freeform display, such as a desktop device. The requester has to be the
+     * top-most activity and the request should be a response to a user input. When getting
+     * fullscreen and receiving corresponding {@link #onConfigurationChanged(Configuration)} and
+     * {@link #onMultiWindowModeChanged(boolean, Configuration)}, the activity should relayout
+     * itself and the system bars' visibilities can be controlled as usual fullscreen apps.
+     *
+     * Calling it again with the exit request can restore the activity to the previous status.
+     * This will only happen when it got into fullscreen through this API.
+     *
+     * If an app wants to be in fullscreen always, it should claim as not being resizable
+     * by setting
+     * <a href="https://developer.android.com/guide/topics/large-screens/multi-window-support#resizeableActivity">
+     * {@code android:resizableActivity="false"}</a> instead of calling this API.
+     *
+     * @param request Can be {@link #FULLSCREEN_MODE_REQUEST_ENTER} or
+     *                {@link #FULLSCREEN_MODE_REQUEST_EXIT} to indicate this request is to get
+     *                fullscreen or get restored.
+     * @param approvalCallback Optional callback, use {@code null} when not necessary. When the
+     *                         request is approved or rejected, the callback will be triggered. This
+     *                         will happen before any configuration change. The callback will be
+     *                         dispatched on the main thread.
+     */
+    public void requestFullscreenMode(@NonNull @FullscreenModeRequest int request,
+            @Nullable OutcomeReceiver<Void, Throwable> approvalCallback) {
+        FullscreenRequestHandler.requestFullscreenMode(
+                request, approvalCallback, mCurrentConfig, getActivityToken());
+    }
+
+    /**
      * Specifies a preference to dock big overlays like the expanded picture-in-picture on TV
      * (see {@link PictureInPictureParams.Builder#setExpandedAspectRatio}). Docking puts the
      * big overlay side-by-side next to this activity, so that both windows are fully visible to
@@ -6612,16 +6654,64 @@
     }
 
     /**
-     * Returns the uid who started this activity.
-     * @hide
+     * Returns the uid of the app that initially launched this activity.
+     *
+     * <p>In order to receive the launching app's uid, at least one of the following has to
+     * be met:
+     * <ul>
+     *     <li>The app must call {@link ActivityOptions#setShareIdentityEnabled(boolean)} with a
+     *     value of {@code true} and launch this activity with the resulting {@code
+     *     ActivityOptions}.
+     *     <li>The launched activity has the same uid as the launching app.
+     *     <li>The launched activity is running in a package that is signed with the same key
+     *     used to sign the platform (typically only system packages such as Settings will
+     *     meet this requirement).
+     * </ul>.
+     * These are the same requirements for {@link #getLaunchedFromPackage()}; if any of these are
+     * met, then these methods can be used to obtain the uid and package name of the launching
+     * app. If none are met, then {@link Process#INVALID_UID} is returned.
+     *
+     * <p>Note, even if the above conditions are not met, the launching app's identity may
+     * still be available from {@link #getCallingPackage()} if this activity was started with
+     * {@code Activity#startActivityForResult} to allow validation of the result's recipient.
+     *
+     * @return the uid of the launching app or {@link Process#INVALID_UID} if the current
+     * activity cannot access the identity of the launching app
+     *
+     * @see ActivityOptions#setShareIdentityEnabled(boolean)
+     * @see #getLaunchedFromPackage()
      */
     public int getLaunchedFromUid() {
         return ActivityClient.getInstance().getLaunchedFromUid(getActivityToken());
     }
 
     /**
-     * Returns the package who started this activity.
-     * @hide
+     * Returns the package name of the app that initially launched this activity.
+     *
+     * <p>In order to receive the launching app's package name, at least one of the following has
+     * to be met:
+     * <ul>
+     *     <li>The app must call {@link ActivityOptions#setShareIdentityEnabled(boolean)} with a
+     *     value of {@code true} and launch this activity with the resulting
+     *     {@code ActivityOptions}.
+     *     <li>The launched activity has the same uid as the launching app.
+     *     <li>The launched activity is running in a package that is signed with the same key
+     *     used to sign the platform (typically only system packages such as Settings will
+     *     meet this requirement).
+     * </ul>.
+     * These are the same requirements for {@link #getLaunchedFromUid()}; if any of these are
+     * met, then these methods can be used to obtain the uid and package name of the launching
+     * app. If none are met, then {@code null} is returned.
+     *
+     * <p>Note, even if the above conditions are not met, the launching app's identity may
+     * still be available from {@link #getCallingPackage()} if this activity was started with
+     * {@code Activity#startActivityForResult} to allow validation of the result's recipient.
+     *
+     * @return the package name of the launching app or null if the current activity
+     * cannot access the identity of the launching app
+     *
+     * @see ActivityOptions#setShareIdentityEnabled(boolean)
+     * @see #getLaunchedFromUid()
      */
     @Nullable
     public String getLaunchedFromPackage() {
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 324b8e7..ce99119 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -24,6 +24,7 @@
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.util.Singleton;
@@ -372,6 +373,14 @@
         }
     }
 
+    void requestMultiwindowFullscreen(IBinder token, int request, IRemoteCallback callback) {
+        try {
+            getActivityClientController().requestMultiwindowFullscreen(token, request, callback);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
     void startLockTaskModeByToken(IBinder token) {
         try {
             getActivityClientController().startLockTaskModeByToken(token);
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 52ef7fb..1b92312 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -202,6 +202,12 @@
     private static final String KEY_LOCK_TASK_MODE = "android:activity.lockTaskMode";
 
     /**
+     * Whether the launching app's identity should be available to the launched activity.
+     * @see #setShareIdentityEnabled(boolean)
+     */
+    private static final String KEY_SHARE_IDENTITY = "android:activity.shareIdentity";
+
+    /**
      * The display id the activity should be launched into.
      * @see #setLaunchDisplayId(int)
      * @hide
@@ -457,6 +463,7 @@
     private int mLaunchTaskId = -1;
     private int mPendingIntentLaunchFlags;
     private boolean mLockTaskMode = false;
+    private boolean mShareIdentity = false;
     private boolean mDisallowEnterPictureInPictureWhileLaunching;
     private boolean mApplyActivityFlagsForBubbles;
     private boolean mTaskAlwaysOnTop;
@@ -1238,6 +1245,7 @@
                 break;
         }
         mLockTaskMode = opts.getBoolean(KEY_LOCK_TASK_MODE, false);
+        mShareIdentity = opts.getBoolean(KEY_SHARE_IDENTITY, false);
         mLaunchDisplayId = opts.getInt(KEY_LAUNCH_DISPLAY_ID, INVALID_DISPLAY);
         mCallerDisplayId = opts.getInt(KEY_CALLER_DISPLAY_ID, INVALID_DISPLAY);
         mLaunchTaskDisplayArea = opts.getParcelable(KEY_LAUNCH_TASK_DISPLAY_AREA_TOKEN, android.window.WindowContainerToken.class);
@@ -1488,6 +1496,20 @@
     }
 
     /**
+     * Returns whether the launching app has opted-in to sharing its identity with the launched
+     * activity.
+     *
+     * @see #setShareIdentityEnabled(boolean)
+     * @see Activity#getLaunchedFromUid()
+     * @see Activity#getLaunchedFromPackage()
+     *
+     * @hide
+     */
+    public boolean getShareIdentity() {
+        return mShareIdentity;
+    }
+
+    /**
      * Gets whether the activity want to be launched as other theme for the splash screen.
      * @hide
      */
@@ -1560,6 +1582,33 @@
     }
 
     /**
+     * Sets whether the identity of the launching app should be shared with the activity.
+     *
+     * <p>Use this option when starting an activity that needs to know the identity of the
+     * launching app; with this set to {@code true}, the activity will have access to the launching
+     * app's package name and uid.
+     *
+     * <p>Defaults to {@code false} if not set.
+     *
+     * <p>Note, even if the launching app does not explicitly enable sharing of its identity, if
+     * the activity is started with {@code Activity#startActivityForResult}, then {@link
+     * Activity#getCallingPackage()} will still return the launching app's package name to
+     * allow validation of the result's recipient. Also, an activity running within a package
+     * signed by the same key used to sign the platform (some system apps such as Settings will
+     * be signed with the platform's key) will have access to the launching app's identity.
+     *
+     * @param shareIdentity whether the launching app's identity should be shared with the activity
+     * @return {@code this} {@link ActivityOptions} instance.
+     * @see Activity#getLaunchedFromPackage()
+     * @see Activity#getLaunchedFromUid()
+     */
+    @NonNull
+    public ActivityOptions setShareIdentityEnabled(boolean shareIdentity) {
+        mShareIdentity = shareIdentity;
+        return this;
+    }
+
+    /**
      * Gets the id of the display where activity should be launched.
      * @return The id of the display where activity should be launched,
      *         {@link android.view.Display#INVALID_DISPLAY} if not set.
@@ -2039,6 +2088,7 @@
                 break;
         }
         mLockTaskMode = otherOptions.mLockTaskMode;
+        mShareIdentity = otherOptions.mShareIdentity;
         mAnimSpecs = otherOptions.mAnimSpecs;
         mAnimationFinishedListener = otherOptions.mAnimationFinishedListener;
         mSpecsFuture = otherOptions.mSpecsFuture;
@@ -2123,6 +2173,9 @@
         if (mLockTaskMode) {
             b.putBoolean(KEY_LOCK_TASK_MODE, mLockTaskMode);
         }
+        if (mShareIdentity) {
+            b.putBoolean(KEY_SHARE_IDENTITY, mShareIdentity);
+        }
         if (mLaunchDisplayId != INVALID_DISPLAY) {
             b.putInt(KEY_LAUNCH_DISPLAY_ID, mLaunchDisplayId);
         }
diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index 7cfca97..be8f48d 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -62,6 +62,12 @@
     public static final int INVALID_TASK_ID = -1;
 
     /**
+     * Invalid windowing mode.
+     * @hide
+     */
+    public static final int INVALID_WINDOWING_MODE = -1;
+
+    /**
      * Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
      * that the resize doesn't need to preserve the window, and can be skipped if bounds
      * is unchanged. This mode is used by window manager in most cases.
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 569f4dd..4d3f9e4 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -3816,8 +3816,17 @@
             @NonNull String targetPackageName) throws NameNotFoundException {
         Objects.requireNonNull(sourcePackageName);
         Objects.requireNonNull(targetPackageName);
+        return canPackageQuery(sourcePackageName, new String[]{targetPackageName})[0];
+    }
+
+    @Override
+    @NonNull
+    public boolean[] canPackageQuery(@NonNull String sourcePackageName,
+            @NonNull String[] targetPackageNames) throws NameNotFoundException {
+        Objects.requireNonNull(sourcePackageName);
+        Objects.requireNonNull(targetPackageNames);
         try {
-            return mPM.canPackageQuery(sourcePackageName, targetPackageName, getUserId());
+            return mPM.canPackageQuery(sourcePackageName, targetPackageNames, getUserId());
         } catch (ParcelableException e) {
             e.maybeRethrow(PackageManager.NameNotFoundException.class);
             throw new RuntimeException(e);
diff --git a/core/java/android/app/FullscreenRequestHandler.java b/core/java/android/app/FullscreenRequestHandler.java
new file mode 100644
index 0000000..52f461d
--- /dev/null
+++ b/core/java/android/app/FullscreenRequestHandler.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.app.Activity.FULLSCREEN_MODE_REQUEST_ENTER;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.OutcomeReceiver;
+
+/**
+ * @hide
+ */
+public class FullscreenRequestHandler {
+    @IntDef(prefix = { "RESULT_" }, value = {
+            RESULT_APPROVED,
+            RESULT_FAILED_NOT_IN_FREEFORM,
+            RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY,
+            RESULT_FAILED_NOT_DEFAULT_FREEFORM,
+            RESULT_FAILED_NOT_TOP_FOCUSED
+    })
+    public @interface RequestResult {}
+
+    public static final int RESULT_APPROVED = 0;
+    public static final int RESULT_FAILED_NOT_IN_FREEFORM = 1;
+    public static final int RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY = 2;
+    public static final int RESULT_FAILED_NOT_DEFAULT_FREEFORM = 3;
+    public static final int RESULT_FAILED_NOT_TOP_FOCUSED = 4;
+
+    public static final String REMOTE_CALLBACK_RESULT_KEY = "result";
+
+    static void requestFullscreenMode(@NonNull @Activity.FullscreenModeRequest int request,
+            @Nullable OutcomeReceiver<Void, Throwable> approvalCallback, Configuration config,
+            IBinder token) {
+        int earlyCheck = earlyCheckRequestMatchesWindowingMode(
+                request, config.windowConfiguration.getWindowingMode());
+        if (earlyCheck != RESULT_APPROVED) {
+            if (approvalCallback != null) {
+                notifyFullscreenRequestResult(approvalCallback, earlyCheck);
+            }
+            return;
+        }
+        try {
+            if (approvalCallback != null) {
+                ActivityClient.getInstance().requestMultiwindowFullscreen(token, request,
+                        new IRemoteCallback.Stub() {
+                            @Override
+                            public void sendResult(Bundle res) {
+                                notifyFullscreenRequestResult(
+                                        approvalCallback, res.getInt(REMOTE_CALLBACK_RESULT_KEY));
+                            }
+                        });
+            } else {
+                ActivityClient.getInstance().requestMultiwindowFullscreen(token, request, null);
+            }
+        } catch (Throwable e) {
+            if (approvalCallback != null) {
+                approvalCallback.onError(e);
+            }
+        }
+    }
+
+    private static void notifyFullscreenRequestResult(
+            OutcomeReceiver<Void, Throwable> callback, int result) {
+        Throwable e = null;
+        switch (result) {
+            case RESULT_FAILED_NOT_IN_FREEFORM:
+                e = new IllegalStateException("The window is not a freeform window, the request "
+                        + "to get into fullscreen cannot be approved.");
+                break;
+            case RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY:
+                e = new IllegalStateException("The window is not in fullscreen by calling the "
+                        + "requestFullscreenMode API before, such that cannot be restored.");
+                break;
+            case RESULT_FAILED_NOT_DEFAULT_FREEFORM:
+                e = new IllegalStateException("The window is not launched in freeform by default.");
+                break;
+            case RESULT_FAILED_NOT_TOP_FOCUSED:
+                e = new IllegalStateException("The window is not the top focused window.");
+                break;
+            default:
+                callback.onResult(null);
+                break;
+        }
+        if (e != null) {
+            callback.onError(e);
+        }
+    }
+
+    private static int earlyCheckRequestMatchesWindowingMode(int request, int windowingMode) {
+        if (request == FULLSCREEN_MODE_REQUEST_ENTER) {
+            if (windowingMode != WINDOWING_MODE_FREEFORM) {
+                return RESULT_FAILED_NOT_IN_FREEFORM;
+            }
+        } else {
+            if (windowingMode != WINDOWING_MODE_FULLSCREEN) {
+                return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
+            }
+        }
+        return RESULT_APPROVED;
+    }
+}
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 8b655b9..286b84c 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -24,6 +24,7 @@
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.os.IRemoteCallback;
 import android.os.PersistableBundle;
 import android.view.RemoteAnimationDefinition;
 import android.window.SizeConfigurationBuckets;
@@ -102,6 +103,8 @@
     void setPictureInPictureParams(in IBinder token, in PictureInPictureParams params);
     oneway void setShouldDockBigOverlays(in IBinder token, in boolean shouldDockBigOverlays);
     void toggleFreeformWindowingMode(in IBinder token);
+    oneway void requestMultiwindowFullscreen(in IBinder token, in int request,
+            in IRemoteCallback callback);
 
     oneway void startLockTaskModeByToken(in IBinder token);
     oneway void stopLockTaskModeByToken(in IBinder token);
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index f63f406..6c43010 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -511,10 +511,26 @@
     @SystemApi
     public static final int MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER = 1;
 
+    /**
+     * State indicating that media transfer to this receiver device is succeeded.
+     *
+     * @hide
+     */
+    public static final int MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED = 2;
+
+    /**
+     * State indicating that media transfer to this receiver device is failed.
+     *
+     * @hide
+     */
+    public static final int MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED = 3;
+
     /** @hide */
     @IntDef(prefix = {"MEDIA_TRANSFER_RECEIVER_STATE_"}, value = {
             MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
             MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
+            MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface MediaTransferReceiverState {}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 1e6412f..128a872 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3918,34 +3918,46 @@
     public static @interface MtePolicy {}
 
     /**
-     * Set MTE policy for device. MTE_ENABLED does not necessarily enable MTE if set on a device
-     * that does not support MTE.
-     *
-     * The default policy is MTE_NOT_CONTROLLED_BY_POLICY.
-     *
-     * Memory Tagging Extension (MTE) is a CPU extension that allows to protect against certain
+     * Called by a device owner or profile owner of an organization-owned device to set the Memory
+     * Tagging Extension (MTE) policy. MTE is a CPU extension that allows to protect against certain
      * classes of security problems at a small runtime performance cost overhead.
      *
-     * @param policy the policy to be set
+     * <p>The MTE policy can only be set to {@link #MTE_DISABLED} if called by a device owner.
+     * Otherwise a {@link SecurityException} will be thrown.
+     *
+     * @throws SecurityException if caller is not device owner or profile owner of org-owned device
+     *     or if called on a parent instance
+     * @param policy the MTE policy to be set
      */
     public void setMtePolicy(@MtePolicy int policy) {
-        // TODO(b/244290023): implement
-        // This is SecurityException to temporarily make ParentProfileTest happy.
-        // This is not used.
-        throw new SecurityException("not implemented");
+        throwIfParentInstance("setMtePolicy");
+        if (mService != null) {
+            try {
+                mService.setMtePolicy(policy);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     /**
-     * Get currently set MTE policy. This is not necessarily the same as the state of MTE on the
-     * device, as the device might not support MTE.
+     * Called by a device owner, a profile owner of an organization-owned device or the system to
+     * get the Memory Tagging Extension (MTE) policy
      *
-     * @return the currently set policy
+     * @throws SecurityException if caller is not device owner or profile owner of org-owned device
+     *                           or system uid, or if called on a parent instance
+     * @return the currently set MTE policy
      */
     public @MtePolicy int getMtePolicy() {
-        // TODO(b/244290023): implement
-        // This is SecurityException to temporarily make ParentProfileTest happy.
-        // This is not used.
-        throw new SecurityException("not implemented");
+        throwIfParentInstance("setMtePolicy");
+        if (mService != null) {
+            try {
+                return mService.getMtePolicy();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        return MTE_NOT_CONTROLLED_BY_POLICY;
     }
 
     // TODO: Expose this as SystemAPI once we add the query API
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 8a40265..5383dca 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -571,4 +571,7 @@
 
     void setApplicationExemptions(String packageName, in int[]exemptions);
     int[] getApplicationExemptions(String packageName);
+
+    void setMtePolicy(int flag);
+    int getMtePolicy();
 }
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 5c47ea2..f17d186 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -33,6 +33,7 @@
 import android.hardware.input.VirtualMouseScrollEvent;
 import android.hardware.input.VirtualTouchEvent;
 import android.hardware.input.VirtualTouchscreenConfig;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
 import android.os.ResultReceiver;
 
 /**
@@ -84,6 +85,10 @@
     void createVirtualTouchscreen(
             in VirtualTouchscreenConfig config,
             IBinder token);
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    void createVirtualNavigationTouchpad(
+            in VirtualNavigationTouchpadConfig 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 57bdf2d..dba7c8e 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -49,6 +49,8 @@
 import android.hardware.input.VirtualKeyboardConfig;
 import android.hardware.input.VirtualMouse;
 import android.hardware.input.VirtualMouseConfig;
+import android.hardware.input.VirtualNavigationTouchpad;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
 import android.hardware.input.VirtualTouchscreen;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.os.Binder;
@@ -660,6 +662,30 @@
         }
 
         /**
+         * Creates a virtual touchpad in navigation mode.
+         *
+         * A touchpad in navigation mode means that its events are interpreted as navigation events
+         * (up, down, etc) instead of using them to update a cursor's absolute position. If the
+         * events are not consumed they are converted to DPAD events.
+         *
+         * @param config the configurations of the virtual navigation touchpad.
+         */
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        public VirtualNavigationTouchpad createVirtualNavigationTouchpad(
+                 @NonNull VirtualNavigationTouchpadConfig config) {
+            try {
+                final IBinder token = new Binder(
+                        "android.hardware.input.VirtualNavigationTouchpad:"
+                            + config.getInputDeviceName());
+                mVirtualDevice.createVirtualNavigationTouchpad(config, token);
+                return new VirtualNavigationTouchpad(mVirtualDevice, token);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Creates a virtual touchscreen.
          *
          * @param display         the display that the events inputted through this device should
@@ -776,11 +802,11 @@
 
         private String getVirtualDisplayName() {
             try {
-                // Currently this just use the association ID, which means all of the virtual
-                // displays created using the same virtual device will have the same name. The name
-                // should only be used for informational purposes, and not for identifying the
-                // display in code.
-                return "VirtualDevice_" + mVirtualDevice.getAssociationId();
+                // Currently this just use the device ID, which means all of the virtual displays
+                // created using the same virtual device will have the same name. The name should
+                // only be used for informational purposes, and not for identifying the display in
+                // code.
+                return "VirtualDevice_" + mVirtualDevice.getDeviceId();
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index ebc00a7..8aa0454 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -6117,9 +6117,8 @@
     public static final String EXTRA_UID = "android.intent.extra.UID";
 
     /**
-     * @hide String array of package names.
+     * String array of package names.
      */
-    @SystemApi
     public static final String EXTRA_PACKAGES = "android.intent.extra.PACKAGES";
 
     /**
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 2c28268..84811ea 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -1528,12 +1528,14 @@
      * the application, e.g. the target SDK version.
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int HIDDEN_API_ENFORCEMENT_DEFAULT = -1;
     /**
      * No API enforcement; the app can access the entire internal private API. Only for use by
      * system apps.
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int HIDDEN_API_ENFORCEMENT_DISABLED = 0;
     /**
      * No API enforcement, but enable the detection logic and warnings. Observed behaviour is the
@@ -1541,11 +1543,13 @@
      * APIs are accessed.
      * @hide
      * */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int HIDDEN_API_ENFORCEMENT_JUST_WARN = 1;
     /**
      * Dark grey list enforcement. Enforces the dark grey and black lists
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int HIDDEN_API_ENFORCEMENT_ENABLED = 2;
 
     private static final int HIDDEN_API_ENFORCEMENT_MIN = HIDDEN_API_ENFORCEMENT_DEFAULT;
diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl
index 1e928bd..115d4b0 100644
--- a/core/java/android/content/pm/IPackageInstaller.aidl
+++ b/core/java/android/content/pm/IPackageInstaller.aidl
@@ -69,4 +69,7 @@
     void setSilentUpdatesThrottleTime(long throttleTimeInSeconds);
     void checkInstallConstraints(String installerPackageName, in List<String> packageNames,
             in PackageInstaller.InstallConstraints constraints, in RemoteCallback callback);
+    void waitForInstallConstraints(String installerPackageName, in List<String> packageNames,
+            in PackageInstaller.InstallConstraints constraints, in IntentSender callback,
+            long timeout);
 }
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 35afe9f..81bea2e 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -797,5 +797,5 @@
 
     void setKeepUninstalledPackages(in List<String> packageList);
 
-    boolean canPackageQuery(String sourcePackageName, String targetPackageName, int userId);
+    boolean[] canPackageQuery(String sourcePackageName, in String[] targetPackageNames, int userId);
 }
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 1f01ae9..f17d8fa 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -30,6 +30,7 @@
 import android.Manifest;
 import android.annotation.CallbackExecutor;
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.DurationMillisLong;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -248,6 +249,24 @@
      */
     public static final String EXTRA_STORAGE_PATH = "android.content.pm.extra.STORAGE_PATH";
 
+    /**
+     * The {@link InstallConstraints} object.
+     *
+     * @see Intent#getParcelableExtra(String, Class)
+     * @see #waitForInstallConstraints(List, InstallConstraints, IntentSender, long)
+     */
+    public static final String EXTRA_INSTALL_CONSTRAINTS =
+            "android.content.pm.extra.INSTALL_CONSTRAINTS";
+
+    /**
+     * The {@link InstallConstraintsResult} object.
+     *
+     * @see Intent#getParcelableExtra(String, Class)
+     * @see #waitForInstallConstraints(List, InstallConstraints, IntentSender, long)
+     */
+    public static final String EXTRA_INSTALL_CONSTRAINTS_RESULT =
+            "android.content.pm.extra.INSTALL_CONSTRAINTS_RESULT";
+
     /** {@hide} */
     @Deprecated
     public static final String EXTRA_PACKAGE_NAMES = "android.content.pm.extra.PACKAGE_NAMES";
@@ -885,6 +904,32 @@
     }
 
     /**
+     * Similar to {@link #checkInstallConstraints(List, InstallConstraints, Executor, Consumer)},
+     * but the callback is invoked only when the constraints are satisfied or after timeout.
+     *
+     * @param callback Called when the constraints are satisfied or after timeout.
+     *                 Intents sent to this callback contain:
+     *                 {@link Intent#EXTRA_PACKAGES} for the input package names,
+     *                 {@link #EXTRA_INSTALL_CONSTRAINTS} for the input constraints,
+     *                 {@link #EXTRA_INSTALL_CONSTRAINTS_RESULT} for the result.
+     * @param timeoutMillis The maximum time to wait, in milliseconds until the constraints are
+     *                      satisfied. Valid range is from 0 to one week. {@code 0} means the
+     *                      callback will be invoked immediately no matter constraints are
+     *                      satisfied or not.
+     */
+    public void waitForInstallConstraints(@NonNull List<String> packageNames,
+            @NonNull InstallConstraints constraints,
+            @NonNull IntentSender callback,
+            @DurationMillisLong long timeoutMillis) {
+        try {
+            mInstaller.waitForInstallConstraints(
+                    mInstallerPackageName, packageNames, constraints, callback, timeoutMillis);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Events for observing session lifecycle.
      * <p>
      * A typical session lifecycle looks like this:
@@ -3807,7 +3852,7 @@
      * Note the constraints are applied transitively. If app Foo is used by app Bar (via shared
      * library or bounded service), the constraints will also be applied to Bar.
      */
-    @DataClass(genParcelable = true, genHiddenConstructor = true)
+    @DataClass(genParcelable = true, genHiddenConstructor = true, genEqualsHashCode=true)
     public static final class InstallConstraints implements Parcelable {
         /**
          * Preset constraints suitable for gentle update.
@@ -3968,6 +4013,41 @@
 
         @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(InstallConstraints other) { ... }
+            // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            @SuppressWarnings("unchecked")
+            InstallConstraints that = (InstallConstraints) o;
+            //noinspection PointlessBooleanExpression
+            return true
+                    && mRequireDeviceIdle == that.mRequireDeviceIdle
+                    && mRequireAppNotForeground == that.mRequireAppNotForeground
+                    && mRequireAppNotInteracting == that.mRequireAppNotInteracting
+                    && mRequireAppNotTopVisible == that.mRequireAppNotTopVisible
+                    && mRequireNotInCall == that.mRequireNotInCall;
+        }
+
+        @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 + Boolean.hashCode(mRequireDeviceIdle);
+            _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotForeground);
+            _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotInteracting);
+            _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotTopVisible);
+            _hash = 31 * _hash + Boolean.hashCode(mRequireNotInCall);
+            return _hash;
+        }
+
+        @Override
+        @DataClass.Generated.Member
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             // You can override field parcelling by defining methods like:
             // void parcelFieldName(Parcel dest, int flags) { ... }
@@ -4023,10 +4103,10 @@
         };
 
         @DataClass.Generated(
-                time = 1668650523752L,
+                time = 1670207178734L,
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
-                inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final  boolean mRequireDeviceIdle\nprivate final  boolean mRequireAppNotForeground\nprivate final  boolean mRequireAppNotInteracting\nprivate final  boolean mRequireAppNotTopVisible\nprivate final  boolean mRequireNotInCall\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate  boolean mRequireDeviceIdle\nprivate  boolean mRequireAppNotForeground\nprivate  boolean mRequireAppNotInteracting\nprivate  boolean mRequireAppNotTopVisible\nprivate  boolean mRequireNotInCall\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireDeviceIdle()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotForeground()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotInteracting()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotTopVisible()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireNotInCall()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true)")
+                inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final  boolean mRequireDeviceIdle\nprivate final  boolean mRequireAppNotForeground\nprivate final  boolean mRequireAppNotInteracting\nprivate final  boolean mRequireAppNotTopVisible\nprivate final  boolean mRequireNotInCall\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate  boolean mRequireDeviceIdle\nprivate  boolean mRequireAppNotForeground\nprivate  boolean mRequireAppNotInteracting\nprivate  boolean mRequireAppNotTopVisible\nprivate  boolean mRequireNotInCall\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireDeviceIdle()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotForeground()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotInteracting()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotTopVisible()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireNotInCall()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genEqualsHashCode=true)")
         @Deprecated
         private void __metadata() {}
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index ec490d1..f3ccfb0 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2979,6 +2979,14 @@
             "android.software.virtualization_framework";
 
     /**
+     * Feature for {@link #getSystemAvailableFeatures()} and {@link #hasSystemFeature(String)}.
+     * This feature indicates whether device supports seamless refresh rate switching.
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_SEAMLESS_REFRESH_RATE_SWITCHING
+            = "android.software.seamless_refresh_rate_switching";
+
+    /**
      * Feature for {@link #getSystemAvailableFeatures} and
      * {@link #hasSystemFeature(String, int)}: If this feature is supported, the Vulkan
      * implementation on this device is hardware accelerated, and the Vulkan native API will
@@ -10388,6 +10396,30 @@
     }
 
     /**
+     * Same as {@link #canPackageQuery(String, String)} but accepts an array of target packages to
+     * be queried.
+     *
+     * @param sourcePackageName The source package that would receive details about the
+     *                          target package.
+     * @param targetPackageNames An array of target packages whose details would be shared with the
+     *                           source package.
+     * @return An array of booleans where each member specifies whether the source package is able
+     * to query for details about the target package given by the corresponding value at the same
+     * index in the array of target packages.
+     * @throws NameNotFoundException if either a given package can not be found on the
+     * system, or if the caller is not able to query for details about the source or
+     * target packages.
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    public boolean[] canPackageQuery(@NonNull String sourcePackageName,
+            @NonNull String[] targetPackageNames) throws NameNotFoundException {
+        throw new UnsupportedOperationException(
+                "canPackageQuery not implemented in subclass");
+    }
+
+    /**
      * Makes a package that provides an authority {@code visibleAuthority} become visible to the
      * application {@code recipientUid}.
      *
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index fd35378..fb61b37 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -46,6 +46,8 @@
     private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings";
     private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy";
     private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts";
+    private static final String ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA =
+            "updateCrossProfileIntentFiltersOnOTA";
 
     /** Index values of each property (to indicate whether they are present in this object). */
     @IntDef(prefix = "INDEX_", value = {
@@ -54,6 +56,7 @@
             INDEX_SHOW_IN_SETTINGS,
             INDEX_INHERIT_DEVICE_POLICY,
             INDEX_USE_PARENTS_CONTACTS,
+            INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA
     })
     @Retention(RetentionPolicy.SOURCE)
     private @interface PropertyIndex {
@@ -63,6 +66,7 @@
     private static final int INDEX_SHOW_IN_SETTINGS = 2;
     private static final int INDEX_INHERIT_DEVICE_POLICY = 3;
     private static final int INDEX_USE_PARENTS_CONTACTS = 4;
+    private static final int INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA = 5;
     /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
     private long mPropertiesPresent = 0;
 
@@ -199,6 +203,7 @@
             // Add items that require exposeAllFields to be true (strictest permission level).
             setStartWithParent(orig.getStartWithParent());
             setInheritDevicePolicy(orig.getInheritDevicePolicy());
+            setUpdateCrossProfileIntentFiltersOnOTA(orig.getUpdateCrossProfileIntentFiltersOnOTA());
         }
         if (hasManagePermission) {
             // Add items that require MANAGE_USERS or stronger.
@@ -354,6 +359,34 @@
      */
     private boolean mUseParentsContacts;
 
+    /**
+     * Returns true if user needs to update default
+     * {@link com.android.server.pm.CrossProfileIntentFilter} with its parents during an OTA update
+     * @hide
+     */
+    public boolean getUpdateCrossProfileIntentFiltersOnOTA() {
+        if (isPresent(INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA)) {
+            return mUpdateCrossProfileIntentFiltersOnOTA;
+        }
+        if (mDefaultProperties != null) {
+            return mDefaultProperties.mUpdateCrossProfileIntentFiltersOnOTA;
+        }
+        throw new SecurityException("You don't have permission to query "
+                + "updateCrossProfileIntentFiltersOnOTA");
+    }
+
+    /** @hide */
+    public void setUpdateCrossProfileIntentFiltersOnOTA(boolean val) {
+        this.mUpdateCrossProfileIntentFiltersOnOTA = val;
+        setPresent(INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA);
+    }
+
+    /*
+     Indicate if {@link com.android.server.pm.CrossProfileIntentFilter}s need to be updated during
+     OTA update between user-parent
+     */
+    private boolean mUpdateCrossProfileIntentFiltersOnOTA;
+
     @Override
     public String toString() {
         // Please print in increasing order of PropertyIndex.
@@ -364,6 +397,8 @@
                 + ", mShowInSettings=" + getShowInSettings()
                 + ", mInheritDevicePolicy=" + getInheritDevicePolicy()
                 + ", mUseParentsContacts=" + getUseParentsContacts()
+                + ", mUpdateCrossProfileIntentFiltersOnOTA="
+                + getUpdateCrossProfileIntentFiltersOnOTA()
                 + "}";
     }
 
@@ -380,6 +415,8 @@
         pw.println(prefix + "    mShowInSettings=" + getShowInSettings());
         pw.println(prefix + "    mInheritDevicePolicy=" + getInheritDevicePolicy());
         pw.println(prefix + "    mUseParentsContacts=" + getUseParentsContacts());
+        pw.println(prefix + "    mUpdateCrossProfileIntentFiltersOnOTA="
+                + getUpdateCrossProfileIntentFiltersOnOTA());
     }
 
     /**
@@ -428,6 +465,9 @@
                 case ATTR_USE_PARENTS_CONTACTS:
                     setUseParentsContacts(parser.getAttributeBoolean(i));
                     break;
+                case ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA:
+                    setUpdateCrossProfileIntentFiltersOnOTA(parser.getAttributeBoolean(i));
+                    break;
                 default:
                     Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
             }
@@ -462,6 +502,11 @@
             serializer.attributeBoolean(null, ATTR_USE_PARENTS_CONTACTS,
                     mUseParentsContacts);
         }
+        if (isPresent(INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA)) {
+            serializer.attributeBoolean(null,
+                    ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA,
+                    mUpdateCrossProfileIntentFiltersOnOTA);
+        }
     }
 
     // For use only with an object that has already had any permission-lacking fields stripped out.
@@ -473,6 +518,7 @@
         dest.writeInt(mShowInSettings);
         dest.writeInt(mInheritDevicePolicy);
         dest.writeBoolean(mUseParentsContacts);
+        dest.writeBoolean(mUpdateCrossProfileIntentFiltersOnOTA);
     }
 
     /**
@@ -488,6 +534,7 @@
         mShowInSettings = source.readInt();
         mInheritDevicePolicy = source.readInt();
         mUseParentsContacts = source.readBoolean();
+        mUpdateCrossProfileIntentFiltersOnOTA = source.readBoolean();
     }
 
     @Override
@@ -517,6 +564,7 @@
         private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT;
         private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO;
         private boolean mUseParentsContacts = false;
+        private boolean mUpdateCrossProfileIntentFiltersOnOTA = false;
 
         public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) {
             mShowInLauncher = showInLauncher;
@@ -546,6 +594,13 @@
             return this;
         }
 
+        /** Sets the value for {@link #mUpdateCrossProfileIntentFiltersOnOTA} */
+        public Builder setUpdateCrossProfileIntentFiltersOnOTA(boolean
+                updateCrossProfileIntentFiltersOnOTA) {
+            mUpdateCrossProfileIntentFiltersOnOTA = updateCrossProfileIntentFiltersOnOTA;
+            return this;
+        }
+
         /** Builds a UserProperties object with *all* values populated. */
         public UserProperties build() {
             return new UserProperties(
@@ -553,7 +608,8 @@
                     mStartWithParent,
                     mShowInSettings,
                     mInheritDevicePolicy,
-                    mUseParentsContacts);
+                    mUseParentsContacts,
+                    mUpdateCrossProfileIntentFiltersOnOTA);
         }
     } // end Builder
 
@@ -563,7 +619,7 @@
             boolean startWithParent,
             @ShowInSettings int showInSettings,
             @InheritDevicePolicy int inheritDevicePolicy,
-            boolean useParentsContacts) {
+            boolean useParentsContacts, boolean updateCrossProfileIntentFiltersOnOTA) {
 
         mDefaultProperties = null;
         setShowInLauncher(showInLauncher);
@@ -571,5 +627,6 @@
         setShowInSettings(showInSettings);
         setInheritDevicePolicy(inheritDevicePolicy);
         setUseParentsContacts(useParentsContacts);
+        setUpdateCrossProfileIntentFiltersOnOTA(updateCrossProfileIntentFiltersOnOTA);
     }
 }
diff --git a/core/java/android/hardware/camera2/CameraExtensionSession.java b/core/java/android/hardware/camera2/CameraExtensionSession.java
index 6f895d5..b0fafea 100644
--- a/core/java/android/hardware/camera2/CameraExtensionSession.java
+++ b/core/java/android/hardware/camera2/CameraExtensionSession.java
@@ -18,6 +18,11 @@
 
 import android.annotation.IntRange;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.camera2.impl.PublicKey;
+import android.hardware.camera2.utils.TypeReference;
+import android.util.Pair;
+import android.util.Range;
 
 import java.util.concurrent.Executor;
 
@@ -424,6 +429,28 @@
     }
 
     /**
+     * Return the realtime still {@link #capture} latency.
+     *
+     * <p>The pair will be in milliseconds with the first value indicating the capture latency from
+     * the {@link ExtensionCaptureCallback#onCaptureStarted} until
+     * {@link ExtensionCaptureCallback#onCaptureProcessStarted}
+     * and the second value containing the estimated post-processing latency from
+     * {@link ExtensionCaptureCallback#onCaptureProcessStarted} until the processed frame returns
+     * to the client.</p>
+     *
+     * <p>The estimations will take into account the current environment conditions, the camera
+     * state and will include the time spent processing the multi-frame capture request along with
+     * any additional time for encoding of the processed buffer if necessary.</p>
+     *
+     * @return The realtime still capture latency,
+     * or {@code null} if the estimation is not supported.
+     */
+    @Nullable
+    public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+        throw new UnsupportedOperationException("Subclasses must override this method");
+    }
+
+    /**
      * Close this capture session asynchronously.
      *
      * <p>Closing a session frees up the target output Surfaces of the session
diff --git a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
index 615536b..360f809 100644
--- a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
@@ -19,6 +19,7 @@
 
 import android.hardware.camera2.extension.CaptureStageImpl;
 import android.hardware.camera2.extension.ICaptureProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
 import android.hardware.camera2.extension.LatencyRange;
 import android.hardware.camera2.extension.Size;
 import android.hardware.camera2.extension.SizeList;
@@ -43,4 +44,5 @@
     CameraMetadataNative getAvailableCaptureRequestKeys();
     CameraMetadataNative getAvailableCaptureResultKeys();
     boolean isCaptureProcessProgressAvailable();
+    @nullable LatencyPair getRealtimeCaptureLatency();
 }
diff --git a/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl b/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
index 0eca5a7..e0f1b64 100644
--- a/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
@@ -19,6 +19,8 @@
 import android.hardware.camera2.extension.CameraSessionConfig;
 import android.hardware.camera2.extension.ICaptureCallback;
 import android.hardware.camera2.extension.IRequestProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
+import android.hardware.camera2.extension.LatencyRange;
 import android.hardware.camera2.extension.OutputSurface;
 
 /** @hide */
@@ -34,4 +36,5 @@
     int startCapture(in ICaptureCallback callback);
     void setParameters(in CaptureRequest captureRequest);
     int startTrigger(in CaptureRequest captureRequest, in ICaptureCallback callback);
+    @nullable LatencyPair getRealtimeCaptureLatency();
 }
diff --git a/core/java/android/hardware/camera2/extension/LatencyPair.aidl b/core/java/android/hardware/camera2/extension/LatencyPair.aidl
new file mode 100644
index 0000000..5174f1d
--- /dev/null
+++ b/core/java/android/hardware/camera2/extension/LatencyPair.aidl
@@ -0,0 +1,23 @@
+/**
+ * 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.camera2.extension;
+
+/** @hide */
+parcelable LatencyPair
+{
+    long first;
+    long second;
+}
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 3a8dc03..42c4411 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -43,6 +43,7 @@
 import android.hardware.camera2.extension.IRequestCallback;
 import android.hardware.camera2.extension.IRequestProcessorImpl;
 import android.hardware.camera2.extension.ISessionProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
 import android.hardware.camera2.extension.OutputConfigId;
 import android.hardware.camera2.extension.OutputSurface;
 import android.hardware.camera2.extension.ParcelCaptureResult;
@@ -61,6 +62,8 @@
 import android.os.HandlerThread;
 import android.os.RemoteException;
 import android.util.Log;
+import android.util.Pair;
+import android.util.Range;
 import android.util.Size;
 import android.view.Surface;
 
@@ -329,6 +332,28 @@
     }
 
     @Override
+    public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+        synchronized (mInterfaceLock) {
+            if (!mInitialized) {
+                throw new IllegalStateException("Uninitialized component");
+            }
+
+            try {
+                LatencyPair latency = mSessionProcessor.getRealtimeCaptureLatency();
+                if (latency != null) {
+                    return new Pair<>(latency.first, latency.second);
+                }
+
+                return null;
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to query realtime latency! Extension service does not "
+                        + "respond");
+                throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
+            }
+        }
+    }
+
+    @Override
     public int setRepeatingRequest(@NonNull CaptureRequest request, @NonNull Executor executor,
             @NonNull ExtensionCaptureCallback listener) throws CameraAccessException {
         int seqId = -1;
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 389c214..259bd7b 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -43,6 +43,7 @@
 import android.hardware.camera2.extension.IPreviewExtenderImpl;
 import android.hardware.camera2.extension.IProcessResultImpl;
 import android.hardware.camera2.extension.IRequestUpdateProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
 import android.hardware.camera2.extension.ParcelImage;
 import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.hardware.camera2.params.ExtensionSessionConfiguration;
@@ -59,6 +60,7 @@
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.Pair;
+import android.util.Range;
 import android.util.Size;
 import android.view.Surface;
 
@@ -466,6 +468,28 @@
     }
 
     @Override
+    public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+        synchronized (mInterfaceLock) {
+            if (!mInitialized) {
+                throw new IllegalStateException("Uninitialized component");
+            }
+
+            try {
+                LatencyPair latency = mImageExtender.getRealtimeCaptureLatency();
+                if (latency != null) {
+                    return new Pair<>(latency.first, latency.second);
+                }
+
+                return null;
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to query realtime latency! Extension service does not "
+                        + "respond");
+                throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
+            }
+        }
+    }
+
+    @Override
     public int setRepeatingRequest(@NonNull CaptureRequest request,
                                    @NonNull Executor executor,
                                    @NonNull ExtensionCaptureCallback listener)
diff --git a/core/java/android/hardware/display/AmbientBrightnessDayStats.java b/core/java/android/hardware/display/AmbientBrightnessDayStats.java
index 8aff911..2816706 100644
--- a/core/java/android/hardware/display/AmbientBrightnessDayStats.java
+++ b/core/java/android/hardware/display/AmbientBrightnessDayStats.java
@@ -208,7 +208,7 @@
     }
 
     private int getBucketIndex(float ambientBrightness) {
-        if (ambientBrightness < mBucketBoundaries[0]) {
+        if (ambientBrightness < mBucketBoundaries[0] || Float.isNaN(ambientBrightness)) {
             return -1;
         }
         int low = 0;
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 829908f..7409187 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -439,9 +439,6 @@
         // 1 (brighter). Set to Float.NaN if there's no override.
         public float screenAutoBrightnessAdjustmentOverride;
 
-        // If true, enables automatic brightness control.
-        public boolean useAutoBrightness;
-
         // If true, scales the brightness to a fraction of desired (as defined by
         // screenLowPowerBrightnessFactor).
         public boolean lowPowerMode;
@@ -471,7 +468,6 @@
             policy = POLICY_BRIGHT;
             useProximitySensor = false;
             screenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            useAutoBrightness = false;
             screenAutoBrightnessAdjustmentOverride = Float.NaN;
             screenLowPowerBrightnessFactor = 0.5f;
             blockScreenOn = false;
@@ -491,7 +487,6 @@
             policy = other.policy;
             useProximitySensor = other.useProximitySensor;
             screenBrightnessOverride = other.screenBrightnessOverride;
-            useAutoBrightness = other.useAutoBrightness;
             screenAutoBrightnessAdjustmentOverride = other.screenAutoBrightnessAdjustmentOverride;
             screenLowPowerBrightnessFactor = other.screenLowPowerBrightnessFactor;
             blockScreenOn = other.blockScreenOn;
@@ -513,7 +508,6 @@
                     && useProximitySensor == other.useProximitySensor
                     && floatEquals(screenBrightnessOverride,
                             other.screenBrightnessOverride)
-                    && useAutoBrightness == other.useAutoBrightness
                     && floatEquals(screenAutoBrightnessAdjustmentOverride,
                             other.screenAutoBrightnessAdjustmentOverride)
                     && screenLowPowerBrightnessFactor
@@ -539,7 +533,6 @@
             return "policy=" + policyToString(policy)
                     + ", useProximitySensor=" + useProximitySensor
                     + ", screenBrightnessOverride=" + screenBrightnessOverride
-                    + ", useAutoBrightness=" + useAutoBrightness
                     + ", screenAutoBrightnessAdjustmentOverride="
                     + screenAutoBrightnessAdjustmentOverride
                     + ", screenLowPowerBrightnessFactor=" + screenLowPowerBrightnessFactor
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpad.java b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
new file mode 100644
index 0000000..2854034
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.companion.virtual.IVirtualDevice;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+/**
+ * A virtual navigation touchpad representing a touch-based input mechanism on a remote device.
+ *
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.
+ *
+ * <p>The virtual touchpad will be in navigation mode. Motion results in focus traversal in the same
+ * manner as D-Pad navigation if the events are not consumed.
+ *
+ * @see android.view.InputDevice#SOURCE_TOUCH_NAVIGATION
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualNavigationTouchpad extends VirtualInputDevice {
+
+    /** @hide */
+    public VirtualNavigationTouchpad(IVirtualDevice virtualDevice, IBinder token) {
+        super(virtualDevice, token);
+    }
+
+    /**
+     * Sends a touch event to the system.
+     *
+     * @param event the event to send
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendTouchEvent(@NonNull VirtualTouchEvent event) {
+        try {
+            mVirtualDevice.sendTouchEvent(mToken, event);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl
new file mode 100644
index 0000000..d912491
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable VirtualNavigationTouchpadConfig;
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
new file mode 100644
index 0000000..f2805bb
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
@@ -0,0 +1,115 @@
+/*
+ * 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.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Configurations to create virtual navigation touchpad.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualNavigationTouchpadConfig extends VirtualInputDeviceConfig
+        implements Parcelable {
+
+    /** The touchpad height. */
+    private final int mHeight;
+    /** The touchpad width. */
+    private final int mWidth;
+
+    private VirtualNavigationTouchpadConfig(@NonNull Builder builder) {
+        super(builder);
+        mHeight = builder.mHeight;
+        mWidth = builder.mWidth;
+    }
+
+    private VirtualNavigationTouchpadConfig(@NonNull Parcel in) {
+        super(in);
+        mHeight = in.readInt();
+        mWidth = in.readInt();
+    }
+
+    /** Returns the touchpad height. */
+    public int getHeight() {
+        return mHeight;
+    }
+
+    /** Returns the touchpad width. */
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeInt(mHeight);
+        dest.writeInt(mWidth);
+    }
+
+    @NonNull
+    public static final Creator<VirtualNavigationTouchpadConfig> CREATOR =
+            new Creator<VirtualNavigationTouchpadConfig>() {
+                @Override
+                public VirtualNavigationTouchpadConfig createFromParcel(Parcel in) {
+                    return new VirtualNavigationTouchpadConfig(in);
+                }
+
+                @Override
+                public VirtualNavigationTouchpadConfig[] newArray(int size) {
+                    return new VirtualNavigationTouchpadConfig[size];
+                }
+            };
+
+    /**
+     * Builder for creating a {@link VirtualNavigationTouchpadConfig}.
+     */
+    public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
+
+        private final int mHeight;
+        private final int mWidth;
+
+        public Builder(@IntRange(from = 1) int touchpadHeight,
+                @IntRange(from = 1) int touchpadWidth) {
+            if (touchpadHeight <= 0 || touchpadWidth <= 0) {
+                throw new IllegalArgumentException(
+                        "Cannot create a virtual navigation touchpad, touchpad dimensions must be "
+                                + "positive. Got: (" + touchpadHeight + ", "
+                                + touchpadWidth + ")");
+            }
+            mHeight = touchpadHeight;
+            mWidth = touchpadWidth;
+        }
+
+        /**
+         * Builds the {@link VirtualNavigationTouchpadConfig} instance.
+         */
+        @NonNull
+        public VirtualNavigationTouchpadConfig build() {
+            return new VirtualNavigationTouchpadConfig(this);
+        }
+    }
+}
diff --git a/core/java/android/hardware/radio/IRadioService.aidl b/core/java/android/hardware/radio/IRadioService.aidl
index 9349cf7..c7131a7 100644
--- a/core/java/android/hardware/radio/IRadioService.aidl
+++ b/core/java/android/hardware/radio/IRadioService.aidl
@@ -31,7 +31,7 @@
     List<RadioManager.ModuleProperties> listModules();
 
     ITuner openTuner(int moduleId, in RadioManager.BandConfig bandConfig, boolean withAudio,
-            in ITunerCallback callback);
+            in ITunerCallback callback, int targetSdkVersion);
 
     ICloseHandle addAnnouncementListener(in int[] enabledTypes,
             in IAnnouncementListener listener);
diff --git a/core/java/android/hardware/radio/ITuner.aidl b/core/java/android/hardware/radio/ITuner.aidl
index 7bf234b..e68c3cc 100644
--- a/core/java/android/hardware/radio/ITuner.aidl
+++ b/core/java/android/hardware/radio/ITuner.aidl
@@ -49,7 +49,7 @@
     /**
      * @throws IllegalStateException if called out of sequence
      */
-    void scan(boolean directionDown, boolean skipSubChannel);
+    void seek(boolean directionDown, boolean skipSubChannel);
 
     /**
      * @throws IllegalArgumentException if invalid arguments are passed
diff --git a/core/java/android/hardware/radio/ITunerCallback.aidl b/core/java/android/hardware/radio/ITunerCallback.aidl
index f98947b..13092cc 100644
--- a/core/java/android/hardware/radio/ITunerCallback.aidl
+++ b/core/java/android/hardware/radio/ITunerCallback.aidl
@@ -24,6 +24,13 @@
 /** {@hide} */
 oneway interface ITunerCallback {
     void onError(int status);
+
+    /**
+     * Callback called when tuning operations, such as tune, step, seek, failed.
+     *
+     * @param result Tuning result of {@link RadioTuner#TunerResultType} type.
+     * @param selector Program selector used for the tuning operation.
+     */
     void onTuneFailed(int result, in ProgramSelector selector);
     void onConfigurationChanged(in RadioManager.BandConfig config);
     void onCurrentProgramInfoChanged(in RadioManager.ProgramInfo info);
@@ -36,6 +43,18 @@
     void onProgramListUpdated(in ProgramList.Chunk chunk);
 
     /**
+     * Callback for passing updates to config flags from {@link IRadioService} to
+     * {@link RadioTuner}.
+     *
+     * @param flag Config flag (defined in {@link RadioManager.ConfigFlag}) updated
+     * @param value Updated value for the config flag
+     */
+    void onConfigFlagUpdated(int flag, boolean value);
+
+    /**
+     * Callback for passing updates to vendor-specific parameter values from
+     * {@link IRadioService} to {@link RadioTuner}.
+     *
      * @param parameters Vendor-specific key-value pairs
      */
     void onParametersUpdated(in Map<String, String> parameters);
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 8a92135..7faa285 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -167,7 +167,10 @@
     public static final int IDENTIFIER_TYPE_HD_STATION_NAME = 10004;
     /**
      * @see {@link IDENTIFIER_TYPE_DAB_SID_EXT}
+     *
+     * @deprecated use {@link #IDENTIFIER_TYPE_DAB_DMB_SID_EXT} instead
      */
+    @Deprecated
     public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5;
     /**
      * 28bit compound primary identifier for Digital Audio Broadcasting.
@@ -183,7 +186,10 @@
      *
      * The remaining bits should be set to zeros when writing on the chip side
      * and ignored when read.
+     *
+     * @deprecated use {@link #IDENTIFIER_TYPE_DAB_DMB_SID_EXT} instead
      */
+    @Deprecated
     public static final int IDENTIFIER_TYPE_DAB_SID_EXT = IDENTIFIER_TYPE_DAB_SIDECC;
     /** 16bit */
     public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6;
@@ -197,7 +203,7 @@
     public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10;
     /**
      * 1: AM, 2:FM
-     * @deprecated use {@link IDENTIFIER_TYPE_DRMO_FREQUENCY} instead
+     * @deprecated use {@link #IDENTIFIER_TYPE_DRMO_FREQUENCY} instead
      */
     @Deprecated
     public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11;
@@ -206,6 +212,23 @@
     /** 0-999 range */
     public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13;
     /**
+     * 44bit compound primary identifier for Digital Audio Broadcasting and
+     * Digital Multimedia Broadcasting.
+     *
+     * <p>Consists of (from the LSB):
+     * - 32bit: SId;
+     * - 8bit: ECC code;
+     * - 4bit: SCIdS.
+     *
+     * <p>SCIdS (Service Component Identifier within the Service) value
+     * of 0 represents the main service, while 1 and above represents
+     * secondary services.
+     *
+     * The remaining bits should be set to zeros when writing on the chip side
+     * and ignored when read.
+     */
+    public static final int IDENTIFIER_TYPE_DAB_DMB_SID_EXT = 14;
+    /**
      * Primary identifier for vendor-specific radio technology.
      * The value format is determined by a vendor.
      *
@@ -219,12 +242,12 @@
      */
     public static final int IDENTIFIER_TYPE_VENDOR_END = PROGRAM_TYPE_VENDOR_END;
     /**
-     * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_START} instead
+     * @deprecated use {@link #IDENTIFIER_TYPE_VENDOR_START} instead
      */
     @Deprecated
     public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = IDENTIFIER_TYPE_VENDOR_START;
     /**
-     * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_END} instead
+     * @deprecated use {@link #IDENTIFIER_TYPE_VENDOR_END} instead
      */
     @Deprecated
     public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = IDENTIFIER_TYPE_VENDOR_END;
@@ -245,6 +268,7 @@
         IDENTIFIER_TYPE_DRMO_MODULATION,
         IDENTIFIER_TYPE_SXM_SERVICE_ID,
         IDENTIFIER_TYPE_SXM_CHANNEL,
+        IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
     })
     @IntRange(from = IDENTIFIER_TYPE_VENDOR_START, to = IDENTIFIER_TYPE_VENDOR_END)
     @Retention(RetentionPolicy.SOURCE)
@@ -285,7 +309,7 @@
      * Type of a radio technology.
      *
      * @return program type.
-     * @deprecated use {@link getPrimaryId} instead
+     * @deprecated use {@link #getPrimaryId} instead
      */
     @Deprecated
     public @ProgramType int getProgramType() {
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index 9a217f9..f072e3b 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -82,6 +82,24 @@
     /** Method return status: time out before operation completion */
     public static final int STATUS_TIMED_OUT = -110;
 
+    /**
+     *  Radio operation status types
+     *
+     * @hide
+     */
+    @IntDef(prefix = { "STATUS_" }, value = {
+            STATUS_OK,
+            STATUS_ERROR,
+            STATUS_PERMISSION_DENIED,
+            STATUS_NO_INIT,
+            STATUS_BAD_VALUE,
+            STATUS_DEAD_OBJECT,
+            STATUS_INVALID_OPERATION,
+            STATUS_TIMED_OUT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RadioStatusType{}
+
 
     // keep in sync with radio_class_t in /system/core/incluse/system/radio.h
     /** Radio module class supporting FM (including HD radio) and AM */
@@ -330,6 +348,7 @@
          * program list.
          * @return the number of audio sources available.
          */
+        @RadioStatusType
         public int getNumAudioSources() {
             return mNumAudioSources;
         }
@@ -1724,6 +1743,7 @@
      * </ul>
      */
     @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioStatusType
     public int listModules(List<ModuleProperties> modules) {
         if (modules == null) {
             Log.e(TAG, "the output list must not be empty");
@@ -1776,7 +1796,7 @@
         ITuner tuner;
         TunerCallbackAdapter halCallback = new TunerCallbackAdapter(callback, handler);
         try {
-            tuner = mService.openTuner(moduleId, config, withAudio, halCallback);
+            tuner = mService.openTuner(moduleId, config, withAudio, halCallback, mTargetSdkVersion);
         } catch (RemoteException | IllegalArgumentException | IllegalStateException ex) {
             Log.e(TAG, "Failed to open tuner", ex);
             return null;
@@ -1853,6 +1873,7 @@
 
     @NonNull private final Context mContext;
     @NonNull private final IRadioService mService;
+    private final int mTargetSdkVersion;
 
     /**
      * @hide
@@ -1869,5 +1890,6 @@
     public RadioManager(Context context, IRadioService service) {
         mContext = context;
         mService = service;
+        mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
     }
 }
diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java
index 969db96..9b2bcde 100644
--- a/core/java/android/hardware/radio/RadioTuner.java
+++ b/core/java/android/hardware/radio/RadioTuner.java
@@ -16,14 +16,20 @@
 
 package android.hardware.radio;
 
+import android.Manifest;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.graphics.Bitmap;
 import android.os.Handler;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executor;
 
 /**
  * RadioTuner interface provides methods to control a radio tuner on the device: selecting and
@@ -43,16 +49,21 @@
     public static final int DIRECTION_DOWN    = 1;
 
     /**
-     * Close the tuner interface. The {@link Callback} callback will not be called
-     * anymore and associated resources will be released.
-     * Must be called when the tuner is not needed to make hardware resources available to others.
+     * Close the tuner interface.
+     *
+     * <p>The {@link Callback} callback will not be called anymore and associated resources will be
+     * released. Must be called when the tuner is not needed to make hardware resources available
+     * to others.
      * */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract void close();
 
     /**
      * Set the active band configuration for this module.
-     * Must be a valid configuration obtained via buildConfig() from a valid BandDescriptor listed
-     * in the ModuleProperties of the module with the specified ID.
+     *
+     * <p>Must be a valid configuration obtained via buildConfig() from a valid BandDescriptor
+     * listed in the ModuleProperties of the module with the specified ID.
+     *
      * @param config The desired band configuration (FmBandConfig or AmBandConfig).
      * @return
      * <ul>
@@ -67,10 +78,13 @@
      * @deprecated Only applicable for HAL 1.x.
      */
     @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
     public abstract int setConfiguration(RadioManager.BandConfig config);
 
     /**
      * Get current configuration.
+     *
      * @param config a BandConfig array of lengh 1 where the configuration is returned.
      * @return
      * <ul>
@@ -86,11 +100,15 @@
      * @deprecated Only applicable for HAL 1.x.
      */
     @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
     public abstract int getConfiguration(RadioManager.BandConfig[] config);
 
 
     /**
-     * Set mute state. When muted, the radio tuner audio source is not available for playback on
+     * Set mute state.
+     *
+     * <p>When muted, the radio tuner audio source is not available for playback on
      * any audio device. when unmuted, the radio tuner audio source is output as a media source
      * and renderd over the audio device selected for media use case.
      * The radio tuner audio source is muted by default when the tuner is first attached.
@@ -107,6 +125,8 @@
      *  service fails, </li>
      * </ul>
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
     public abstract int setMute(boolean mute);
 
     /**
@@ -115,13 +135,19 @@
      * @return {@code true} if the radio tuner audio source is muted or a problem occured
      * retrieving the mute state, {@code false} otherwise.
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract boolean getMute();
 
     /**
      * Step up or down by one channel spacing.
-     * The operation is asynchronous and {@link Callback}
-     * onProgramInfoChanged() will be called when step completes or
-     * onError() when cancelled or timeout.
+     *
+     * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+     * will be called when step completes or {@link Callback#onTuneFailed}
+     * when timeout or canceled.
+     *
+     * <p>When this operation is called by users other than current user or system user, it is
+     * ignored silently.
+     *
      * @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}.
      * @param skipSubChannel indicates to skip sub channels when the configuration currently
      * selected supports sub channel (e.g HD Radio). N/A otherwise.
@@ -136,13 +162,50 @@
      *  service fails, </li>
      * </ul>
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
     public abstract int step(int direction, boolean skipSubChannel);
 
     /**
      * Scan up or down to next valid station.
-     * The operation is asynchronous and {@link Callback}
-     * onProgramInfoChanged() will be called when scan completes or
-     * onError() when cancelled or timeout.
+     *
+     * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+     * will be called when scan completes or {@link Callback#onTuneFailed}
+     * when timeout or canceled.
+     *
+     * <p>When this operation is called by users other than current user or system user, it is
+     * ignored silently.
+     *
+     * @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}.
+     * @param skipSubChannel indicates to skip sub channels when the configuration currently
+     * selected supports sub channel (e.g HD Radio). N/A otherwise.
+     * @return
+     * <ul>
+     *  <li>{@link RadioManager#STATUS_OK} in case of success, </li>
+     *  <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li>
+     *  <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li>
+     *  <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li>
+     *  <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li>
+     *  <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native
+     *  service fails, </li>
+     * </ul>
+     * @deprecated Use {@link #seek(int, boolean)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
+    public abstract int scan(int direction, boolean skipSubChannel);
+
+    /**
+     * Seek up or down to next valid station.
+     *
+     * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+     * will be called when seek completes or {@link Callback#onTuneFailed}
+     * when timeout or canceled.
+     *
+     * <p>When this operation is called by users other than current user or system user, it is
+     * ignore silently.
+     *
      * @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}.
      * @param skipSubChannel indicates to skip sub channels when the configuration currently
      * selected supports sub channel (e.g HD Radio). N/A otherwise.
@@ -157,13 +220,22 @@
      *  service fails, </li>
      * </ul>
      */
-    public abstract int scan(int direction, boolean skipSubChannel);
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
+    public int seek(int direction, boolean skipSubChannel) {
+        throw new UnsupportedOperationException("Seeking is not supported");
+    }
 
     /**
      * Tune to a specific frequency.
-     * The operation is asynchronous and {@link Callback}
-     * onProgramInfoChanged() will be called when tune completes or
-     * onError() when cancelled or timeout.
+     *
+     * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+     * will be called when tune completes or {@link Callback#onTuneFailed}
+     * when timeout or canceled.
+     *
+     * <p>When this operation is called by users other than current user or system user, it is
+     * ignored silently.
+     *
      * @param channel the specific channel or frequency to tune to.
      * @param subChannel the specific sub-channel to tune to. N/A if the selected configuration
      * does not support cub channels.
@@ -177,25 +249,37 @@
      *  <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native
      *  service fails, </li>
      * </ul>
-     * @deprecated Use {@link tune(ProgramSelector)} instead.
+     * @deprecated Use {@link #tune(ProgramSelector)} instead.
      */
     @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
     public abstract int tune(int channel, int subChannel);
 
     /**
      * Tune to a program.
      *
-     * The operation is asynchronous and {@link Callback} onProgramInfoChanged() will be called
-     * when tune completes or onError() when cancelled or on timeout.
+     * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+     * will be called when tune completes or {@link Callback#onTuneFailed}
+     * when timeout or canceled.
+     *
+     * <p>When this operation is called by users other than current user or system user, it is
+     * ignored silently.
      *
      * @throws IllegalArgumentException if the provided selector is invalid
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract void tune(@NonNull ProgramSelector selector);
 
     /**
      * Cancel a pending scan or tune operation.
-     * If an operation is pending, {@link Callback} onError() will be called with
+     *
+     * <p>If an operation is pending, {@link Callback#onTuneFailed} will be called with
      * {@link #ERROR_CANCELLED}.
+     *
+     * <p>When this operation is called by users other than current user or system
+     * user, it is ignored silently.
+     *
      * @return
      * <ul>
      *  <li>{@link RadioManager#STATUS_OK} in case of success, </li>
@@ -207,21 +291,27 @@
      *  service fails, </li>
      * </ul>
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
     public abstract int cancel();
 
     /**
      * Cancels traffic or emergency announcement.
      *
-     * If there was no announcement to cancel, no action is taken.
+     * <p>If there was no announcement to cancel, no action is taken.
      *
-     * There is a race condition between calling cancelAnnouncement and the actual announcement
+     * <p>There is a race condition between calling cancelAnnouncement and the actual announcement
      * being finished, so onTrafficAnnouncement / onEmergencyAnnouncement callback should be
      * tracked with proper locking.
+     * @deprecated Only applicable for HAL 1.x.
      */
+    @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract void cancelAnnouncement();
 
     /**
      * Get current station information.
+     *
      * @param info a ProgramInfo array of lengh 1 where the information is returned.
      * @return
      * <ul>
@@ -233,23 +323,27 @@
      *  <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native
      *  service fails, </li>
      * </ul>
-     * @deprecated Use {@link onProgramInfoChanged} callback instead.
+     * @deprecated Use {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)} callback
+     * instead.
      */
     @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+    @RadioManager.RadioStatusType
     public abstract int getProgramInformation(RadioManager.ProgramInfo[] info);
 
     /**
      * Retrieves a {@link Bitmap} for the given image ID or null,
      * if the image was missing from the tuner.
      *
-     * This involves doing a call to the tuner, so the bitmap should be cached
+     * <p>This involves doing a call to the tuner, so the bitmap should be cached
      * on the application side.
      *
-     * If the method returns null for non-zero ID, it means the image was
+     * <p>If the method returns null for non-zero ID, it means the image was
      * updated on the tuner side. There is a race conditon between fetching
      * image for an old ID and tuner updating the image (and cleaning up the
      * old image). In such case, a new ProgramInfo with updated image id will
-     * be sent with a {@link onProgramInfoChanged} callback.
+     * be sent with a {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)}
+     * callback.
      *
      * @param id The image identifier, retrieved with
      *           {@link RadioMetadata#getBitmapId(String)}.
@@ -258,14 +352,16 @@
      * @hide This API is not thoroughly elaborated yet
      */
     @SuppressWarnings("HiddenAbstractMethod")
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract @Nullable Bitmap getMetadataImage(int id);
 
     /**
      * Initiates a background scan to update internally cached program list.
      *
-     * It may not be necessary to initiate the scan explicitly - the scan MAY be performed on boot.
+     * <p>It may not be necessary to initiate the scan explicitly - the scan MAY be performed on
+     * boot.
      *
-     * The operation is asynchronous and {@link Callback} backgroundScanComplete or onError will
+     * <p>The operation is asynchronous and {@link Callback} backgroundScanComplete or onError will
      * be called if the return value of this call was {@code true}. As result of this call
      * programListChanged may be triggered (if the scanned list differs).
      *
@@ -273,13 +369,16 @@
      * is unavailable; ie. temporarily due to ongoing foreground playback in single-tuner device
      * or permanently if the feature is not supported
      * (see ModuleProperties#isBackgroundScanningSupported()).
+     * @deprecated Only applicable for HAL 1.x.
      */
+    @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract boolean startBackgroundScan();
 
     /**
      * Get the list of discovered radio stations.
      *
-     * To get the full list, set filter to null or empty map.
+     * <p>To get the full list, set filter to null or empty map.
      * Keys must be prefixed with unique vendor Java-style namespace,
      * eg. 'com.somecompany.parameter1'.
      *
@@ -288,24 +387,27 @@
      * @throws IllegalStateException if the scan is in progress or has not been started,
      *         startBackgroundScan() call may fix it.
      * @throws IllegalArgumentException if the vendorFilter argument is not valid.
-     * @deprecated Use {@link getDynamicProgramList} instead.
+     * @deprecated Use {@link #getDynamicProgramList(ProgramList.Filter)} instead.
      */
     @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract @NonNull List<RadioManager.ProgramInfo>
             getProgramList(@Nullable Map<String, String> vendorFilter);
 
     /**
      * Get the dynamic list of discovered radio stations.
      *
-     * The list object is updated asynchronously; to get the updates register
-     * with {@link ProgramList#addListCallback}.
+     * <p>The list object is updated asynchronously; to get the updates register
+     * with {@link ProgramList#registerListCallback(ProgramList.ListCallback)}
+     * or {@link ProgramList#registerListCallback(Executor, ProgramList.ListCallback)}.
      *
-     * When the returned object is no longer used, it must be closed.
+     * <p>When the returned object is no longer used, it must be closed.
      *
      * @param filter filter for the list, or null to get the full list.
      * @return the dynamic program list object, close it after use
      *         or {@code null} if program list is not supported by the tuner
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
         return null;
     }
@@ -316,26 +418,28 @@
      * @throws IllegalStateException if the switch is not supported at current
      *         configuration.
      * @return {@code true} if analog is forced, {@code false} otherwise.
-     * @deprecated Use {@link isConfigFlagSet(int)} instead.
+     * @deprecated Use {@link #isConfigFlagSet(int)} instead.
      */
     @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract boolean isAnalogForced();
 
     /**
      * Forces the analog playback for the supporting radio technology.
      *
-     * User may disable digital playback for FM HD Radio or hybrid FM/DAB with
+     * <p>User may disable digital playback for FM HD Radio or hybrid FM/DAB with
      * this option. This is purely user choice, ie. does not reflect digital-
      * analog handover managed from the HAL implementation side.
      *
-     * Some radio technologies may not support this, ie. DAB.
+     * <p>Some radio technologies may not support this, ie. DAB.
      *
      * @param isForced {@code true} to force analog, {@code false} for a default behaviour.
      * @throws IllegalStateException if the switch is not supported at current
      *         configuration.
-     * @deprecated Use {@link setConfigFlag(int, boolean)} instead.
+     * @deprecated Use {@link #setConfigFlag(int, boolean)}  instead.
      */
     @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract void setAnalogForced(boolean isForced);
 
     /**
@@ -344,6 +448,7 @@
      * @param flag Flag to check.
      * @return True, if the flag is supported.
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public boolean isConfigFlagSupported(@RadioManager.ConfigFlag int flag) {
         return false;
     }
@@ -351,29 +456,34 @@
     /**
      * Fetches the current setting of a given config flag.
      *
-     * The success/failure result is consistent with isConfigFlagSupported.
+     * <p>The success/failure result is consistent with isConfigFlagSupported.
      *
      * @param flag Flag to fetch.
      * @return The current value of the flag.
      * @throws IllegalStateException if the flag is not applicable right now.
      * @throws UnsupportedOperationException if the flag is not supported at all.
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public boolean isConfigFlagSet(@RadioManager.ConfigFlag int flag) {
-        throw new UnsupportedOperationException();
+        throw new UnsupportedOperationException("isConfigFlagSet is not supported");
     }
 
     /**
      * Sets the config flag.
      *
-     * The success/failure result is consistent with isConfigFlagSupported.
+     * <p>The success/failure result is consistent with isConfigFlagSupported.
+     *
+     * <p>When this operation is called by users other than current user or system user,
+     * it is ignored silently.
      *
      * @param flag Flag to set.
      * @param value The new value of a given flag.
      * @throws IllegalStateException if the flag is not applicable right now.
      * @throws UnsupportedOperationException if the flag is not supported at all.
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public void setConfigFlag(@RadioManager.ConfigFlag int flag, boolean value) {
-        throw new UnsupportedOperationException();
+        throw new UnsupportedOperationException("Setting config flag is not supported");
     }
 
     /**
@@ -381,30 +491,37 @@
      * The framework does not interpret the parameters, they are passed
      * in an opaque manner between a vendor application and HAL.
      *
-     * Framework does not make any assumptions on the keys or values, other than
+     * <p>Framework does not make any assumptions on the keys or values, other than
      * ones stated in VendorKeyValue documentation (a requirement of key
      * prefixes).
-     * See VendorKeyValue at hardware/interfaces/broadcastradio/2.0/types.hal.
+     * See VendorKeyValue at hardware/interfaces/broadcastradio/2.0/types.hal for
+     * HIDL 2.0 HAL or
+     * hardware/interfaces/broadcastradio/aidl/android/hardware/broadcastradio/VendorKeyValue.aidl
+     * for AIDL HAL.
      *
-     * For each pair in the result map, the key will be one of the keys
+     * <p>For each pair in the result map, the key will be one of the keys
      * contained in the input (possibly with wildcards expanded), and the value
      * will be a vendor-specific result status (such as "OK" or an error code).
      * The implementation may choose to return an empty map, or only return
      * a status for a subset of the provided inputs, at its discretion.
      *
-     * Application and HAL must not use keys with unknown prefix. In particular,
+     * <p>Application and HAL must not use keys with unknown prefix. In particular,
      * it must not place a key-value pair in results vector for unknown key from
      * parameters vector - instead, an unknown key should simply be ignored.
      * In other words, results vector may contain a subset of parameter keys
      * (however, the framework doesn't enforce a strict subset - the only
      * formal requirement is vendor domain prefix for keys).
      *
+     * <p>When this operation is called by users other than current user or system user,
+     * it is ignored silently.
+     *
      * @param parameters Vendor-specific key-value pairs.
      * @return Operation completion status for parameters being set.
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public @NonNull Map<String, String>
             setParameters(@NonNull Map<String, String> parameters) {
-        throw new UnsupportedOperationException();
+        throw new UnsupportedOperationException("Setting parameters is not supported");
     }
 
     /**
@@ -412,23 +529,24 @@
      * The framework does not interpret the parameters, they are passed
      * in an opaque manner between a vendor application and HAL.
      *
-     * Framework does not cache set/get requests, so it's possible for
+     * <p>Framework does not cache set/get requests, so it's possible for
      * getParameter to return a different value than previous setParameter call.
      *
-     * The syntax and semantics of keys are up to the vendor (as long as prefix
+     * <p>The syntax and semantics of keys are up to the vendor (as long as prefix
      * rules are obeyed). For instance, vendors may include some form of
      * wildcard support. In such case, result vector may be of different size
      * than requested keys vector. However, wildcards are not recognized by
      * framework and they are passed as-is to the HAL implementation.
      *
-     * Unknown keys must be ignored and not placed into results vector.
+     * <p>Unknown keys must be ignored and not placed into results vector.
      *
      * @param keys Parameter keys to fetch.
      * @return Vendor-specific key-value pairs.
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public @NonNull Map<String, String>
             getParameters(@NonNull List<String> keys) {
-        throw new UnsupportedOperationException();
+        throw new UnsupportedOperationException("Getting parameters is not supported");
     }
 
     /**
@@ -436,14 +554,16 @@
      * Only valid if a configuration has been applied.
      * @return {@code true} if the antenna is connected, {@code false} otherwise.
      *
-     * @deprecated Use {@link onAntennaState} callback instead
+     * @deprecated Use {@link Callback#onAntennaState(boolean)} callback instead
      */
     @Deprecated
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract boolean isAntennaConnected();
 
     /**
      * Indicates if this client actually controls the tuner.
-     * Control is always granted after
+     *
+     * <p>Control is always granted after
      * {@link RadioManager#openTuner(int,
      * RadioManager.BandConfig, boolean, Callback, Handler)}
      * returns a non null tuner interface.
@@ -451,48 +571,102 @@
      * When this happens, {@link Callback#onControlChanged(boolean)} is received.
      * The client can either wait for control to be returned (which is indicated by the same
      * callback) or close and reopen the tuner interface.
+     *
      * @return {@code true} if this interface controls the tuner,
      * {@code false} otherwise or if a problem occured retrieving the state.
      */
+    @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
     public abstract boolean hasControl();
 
     /** Indicates a failure of radio IC or driver.
-     * The application must close and re open the tuner
-     * @deprecated See {@link onError} callback.
+     *
+     * <p>The application must close and re open the tuner
+     *
+     * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
      */
     @Deprecated
     public static final int ERROR_HARDWARE_FAILURE = 0;
     /** Indicates a failure of the radio service.
-     * The application must close and re open the tuner
-     * @deprecated See {@link onError} callback.
+     *
+     * <p>The application must close and re open the tuner
+     * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
      */
     @Deprecated
     public static final  int ERROR_SERVER_DIED = 1;
     /** A pending seek or tune operation was cancelled
-     * @deprecated See {@link onError} callback.
+     * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
      */
     @Deprecated
     public static final  int ERROR_CANCELLED = 2;
     /** A pending seek or tune operation timed out
-     * @deprecated See {@link onError} callback.
+     * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
      */
     @Deprecated
     public static final  int ERROR_SCAN_TIMEOUT = 3;
     /** The requested configuration could not be applied
-     * @deprecated See {@link onError} callback.
+     * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
      */
     @Deprecated
     public static final  int ERROR_CONFIG = 4;
     /** Background scan was interrupted due to hardware becoming temporarily unavailable.
-     * @deprecated See {@link onError} callback.
+     * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
      */
     @Deprecated
     public static final int ERROR_BACKGROUND_SCAN_UNAVAILABLE = 5;
     /** Background scan failed due to other error, ie. HW failure.
-     * @deprecated See {@link onError} callback.
+     * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
      */
     @Deprecated
     public static final int ERROR_BACKGROUND_SCAN_FAILED = 6;
+    /** Result when a tune, seek, or step operation runs without error.
+     */
+    public static final int TUNER_RESULT_OK = 0;
+    /** Result when internal error occurs in HAL.
+     * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+     */
+    public static final int TUNER_RESULT_INTERNAL_ERROR = 1;
+    /** Result used when the input argument for the method is invalid.
+     * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+     */
+    public static final int TUNER_RESULT_INVALID_ARGUMENTS = 2;
+    /** Result when HAL is of invalid state.
+     * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+     */
+    public static final int TUNER_RESULT_INVALID_STATE = 3;
+    /** Result when the operation is not supported.
+     * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+     */
+    public static final int TUNER_RESULT_NOT_SUPPORTED = 4;
+    /** Result when a tune, seek, or step operation is timeout
+     * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+     */
+    public static final int TUNER_RESULT_TIMEOUT = 5;
+    /** Result when a tune, seek, or step operation is canceled before processed.
+     * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+     */
+    public static final int TUNER_RESULT_CANCELED = 6;
+    /** Result when a tune, seek, or step operation fails due to unknown error.
+     * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+     */
+    public static final int TUNER_RESULT_UNKNOWN_ERROR = 7;
+
+    /**
+     *  Tuning operation result types
+     *
+     * @hide
+     */
+    @IntDef(prefix = { "TUNER_RESULT_" }, value = {
+            TUNER_RESULT_OK,
+            TUNER_RESULT_INTERNAL_ERROR,
+            TUNER_RESULT_INVALID_ARGUMENTS,
+            TUNER_RESULT_INVALID_STATE,
+            TUNER_RESULT_NOT_SUPPORTED,
+            TUNER_RESULT_TIMEOUT,
+            TUNER_RESULT_CANCELED,
+            TUNER_RESULT_UNKNOWN_ERROR,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TunerResultType{}
 
     /**
      * Callback provided by the client application when opening a {@link RadioTuner}
@@ -506,8 +680,9 @@
          * {@link #ERROR_CANCELLED}, {@link #ERROR_SCAN_TIMEOUT},
          * {@link #ERROR_CONFIG}
          *
-         * @deprecated Use {@link onTuneFailed} for tune, scan and step;
-         *             other use cases (configuration, background scan) are already deprecated.
+         * @deprecated Use {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} for
+         *             tune, scan and step; other use cases (configuration, background scan)
+         *             are already deprecated.
          */
         public void onError(int status) {}
 
@@ -518,7 +693,7 @@
          * @param selector ProgramSelector argument of tune that failed;
          *                 null for scan and step.
          */
-        public void onTuneFailed(int result, @Nullable ProgramSelector selector) {}
+        public void onTuneFailed(@TunerResultType int result, @Nullable ProgramSelector selector) {}
 
         /**
          * onConfigurationChanged() is called upon successful completion of
@@ -533,7 +708,7 @@
         /**
          * Called when program info (including metadata) for the current program has changed.
          *
-         * It happens either upon successful completion of {@link RadioTuner#step(int, boolean)},
+         * <p>It happens either upon successful completion of {@link RadioTuner#step(int, boolean)},
          * {@link RadioTuner#scan(int, boolean)}, {@link RadioTuner#tune(int, int)}; when
          * a switching to alternate frequency occurs; or when metadata is updated.
          */
@@ -589,16 +764,29 @@
         /**
          * Called when available program list changed.
          *
-         * Use {@link RadioTuner#getProgramList(String)} to get an actual list.
+         * Use {@link RadioTuner#getProgramList(Map)} to get an actual list.
          */
         public void onProgramListChanged() {}
 
         /**
+         * Called when config flags are updated asynchronously due to internal events
+         * in broadcast radio HAL.
+         *
+         * {@link RadioTuner#setConfigFlag(int, boolean)} must not trigger this
+         * callback.
+         *
+         * @param flag Config flag updated
+         * @param value Value of the updated config flag
+         */
+        public void onConfigFlagUpdated(@RadioManager.ConfigFlag int flag, boolean value) {}
+
+        /**
          * Generic callback for passing updates to vendor-specific parameter values.
-         * The framework does not interpret the parameters, they are passed
+         *
+         * <p>The framework does not interpret the parameters, they are passed
          * in an opaque manner between a vendor application and HAL.
          *
-         * It's up to the HAL implementation if and how to implement this callback,
+         * <p>It's up to the HAL implementation if and how to implement this callback,
          * as long as it obeys the prefix rule. In particular, only selected keys
          * may be notified this way. However, setParameters must not trigger
          * this callback, while an internal event can change parameters
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
index 4a18333..bdbca91 100644
--- a/core/java/android/hardware/radio/TunerAdapter.java
+++ b/core/java/android/hardware/radio/TunerAdapter.java
@@ -154,7 +154,7 @@
     @Override
     public int scan(int direction, boolean skipSubChannel) {
         try {
-            mTuner.scan(/* directionDown= */ direction == RadioTuner.DIRECTION_DOWN,
+            mTuner.seek(/* directionDown= */ direction == RadioTuner.DIRECTION_DOWN,
                     skipSubChannel);
         } catch (IllegalStateException e) {
             Log.e(TAG, "Can't scan", e);
@@ -167,6 +167,21 @@
     }
 
     @Override
+    public int seek(int direction, boolean skipSubChannel) {
+        try {
+            mTuner.seek(/* directionDown= */ direction == RadioTuner.DIRECTION_DOWN,
+                    skipSubChannel);
+        } catch (IllegalStateException e) {
+            Log.e(TAG, "Can't seek", e);
+            return RadioManager.STATUS_INVALID_OPERATION;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Service died", e);
+            return RadioManager.STATUS_DEAD_OBJECT;
+        }
+        return RadioManager.STATUS_OK;
+    }
+
+    @Override
     public int tune(int channel, int subChannel) {
         try {
             int band;
diff --git a/core/java/android/hardware/radio/TunerCallbackAdapter.java b/core/java/android/hardware/radio/TunerCallbackAdapter.java
index b9782a8..22f5902 100644
--- a/core/java/android/hardware/radio/TunerCallbackAdapter.java
+++ b/core/java/android/hardware/radio/TunerCallbackAdapter.java
@@ -246,6 +246,11 @@
     }
 
     @Override
+    public void onConfigFlagUpdated(@RadioManager.ConfigFlag int flag, boolean value) {
+        mHandler.post(() -> mCallback.onConfigFlagUpdated(flag, value));
+    }
+
+    @Override
     public void onParametersUpdated(Map<String, String> parameters) {
         mHandler.post(() -> mCallback.onParametersUpdated(parameters));
     }
diff --git a/core/java/android/net/vcn/VcnConfig.java b/core/java/android/net/vcn/VcnConfig.java
index fd3fe37..dcf0026 100644
--- a/core/java/android/net/vcn/VcnConfig.java
+++ b/core/java/android/net/vcn/VcnConfig.java
@@ -15,16 +15,23 @@
  */
 package android.net.vcn;
 
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
 import static com.android.internal.annotations.VisibleForTesting.Visibility;
+import static com.android.server.vcn.util.PersistableBundleUtils.INTEGER_DESERIALIZER;
+import static com.android.server.vcn.util.PersistableBundleUtils.INTEGER_SERIALIZER;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.util.ArraySet;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
@@ -32,6 +39,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.Objects;
 import java.util.Set;
 
@@ -46,22 +54,36 @@
 public final class VcnConfig implements Parcelable {
     @NonNull private static final String TAG = VcnConfig.class.getSimpleName();
 
+    private static final Set<Integer> ALLOWED_TRANSPORTS = new ArraySet<>();
+
+    static {
+        ALLOWED_TRANSPORTS.add(TRANSPORT_WIFI);
+        ALLOWED_TRANSPORTS.add(TRANSPORT_CELLULAR);
+    }
+
     private static final String PACKAGE_NAME_KEY = "mPackageName";
     @NonNull private final String mPackageName;
 
     private static final String GATEWAY_CONNECTION_CONFIGS_KEY = "mGatewayConnectionConfigs";
     @NonNull private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs;
 
+    private static final Set<Integer> RESTRICTED_TRANSPORTS_DEFAULT =
+            Collections.singleton(TRANSPORT_WIFI);
+    private static final String RESTRICTED_TRANSPORTS_KEY = "mRestrictedTransports";
+    @NonNull private final Set<Integer> mRestrictedTransports;
+
     private static final String IS_TEST_MODE_PROFILE_KEY = "mIsTestModeProfile";
     private final boolean mIsTestModeProfile;
 
     private VcnConfig(
             @NonNull String packageName,
             @NonNull Set<VcnGatewayConnectionConfig> gatewayConnectionConfigs,
+            @NonNull Set<Integer> restrictedTransports,
             boolean isTestModeProfile) {
         mPackageName = packageName;
         mGatewayConnectionConfigs =
                 Collections.unmodifiableSet(new ArraySet<>(gatewayConnectionConfigs));
+        mRestrictedTransports = Collections.unmodifiableSet(new ArraySet<>(restrictedTransports));
         mIsTestModeProfile = isTestModeProfile;
 
         validate();
@@ -82,6 +104,20 @@
                 new ArraySet<>(
                         PersistableBundleUtils.toList(
                                 gatewayConnectionConfigsBundle, VcnGatewayConnectionConfig::new));
+
+        final PersistableBundle restrictedTransportsBundle =
+                in.getPersistableBundle(RESTRICTED_TRANSPORTS_KEY);
+        if (restrictedTransportsBundle == null) {
+            // RESTRICTED_TRANSPORTS_KEY was added in U and does not exist in VcnConfigs created in
+            // older platforms
+            mRestrictedTransports = RESTRICTED_TRANSPORTS_DEFAULT;
+        } else {
+            mRestrictedTransports =
+                    new ArraySet<Integer>(
+                            PersistableBundleUtils.toList(
+                                    restrictedTransportsBundle, INTEGER_DESERIALIZER));
+        }
+
         mIsTestModeProfile = in.getBoolean(IS_TEST_MODE_PROFILE_KEY);
 
         validate();
@@ -91,6 +127,19 @@
         Objects.requireNonNull(mPackageName, "packageName was null");
         Preconditions.checkCollectionNotEmpty(
                 mGatewayConnectionConfigs, "gatewayConnectionConfigs was empty");
+
+        final Iterator<Integer> iterator = mRestrictedTransports.iterator();
+        while (iterator.hasNext()) {
+            final int transport = iterator.next();
+            if (!ALLOWED_TRANSPORTS.contains(transport)) {
+                iterator.remove();
+                Log.w(
+                        TAG,
+                        "Found invalid transport "
+                                + transport
+                                + " which might be from a new version of VcnConfig");
+            }
+        }
     }
 
     /**
@@ -110,6 +159,16 @@
     }
 
     /**
+     * Retrieve the transports that will be restricted by the VCN.
+     *
+     * @see Builder#setRestrictedUnderlyingNetworkTransports(Set)
+     */
+    @NonNull
+    public Set<Integer> getRestrictedUnderlyingNetworkTransports() {
+        return Collections.unmodifiableSet(mRestrictedTransports);
+    }
+
+    /**
      * Returns whether or not this VcnConfig is restricted to test networks.
      *
      * @hide
@@ -134,6 +193,12 @@
                         new ArrayList<>(mGatewayConnectionConfigs),
                         VcnGatewayConnectionConfig::toPersistableBundle);
         result.putPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY, gatewayConnectionConfigsBundle);
+
+        final PersistableBundle restrictedTransportsBundle =
+                PersistableBundleUtils.fromList(
+                        new ArrayList<>(mRestrictedTransports), INTEGER_SERIALIZER);
+        result.putPersistableBundle(RESTRICTED_TRANSPORTS_KEY, restrictedTransportsBundle);
+
         result.putBoolean(IS_TEST_MODE_PROFILE_KEY, mIsTestModeProfile);
 
         return result;
@@ -141,7 +206,8 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mPackageName, mGatewayConnectionConfigs, mIsTestModeProfile);
+        return Objects.hash(
+                mPackageName, mGatewayConnectionConfigs, mRestrictedTransports, mIsTestModeProfile);
     }
 
     @Override
@@ -153,6 +219,7 @@
         final VcnConfig rhs = (VcnConfig) other;
         return mPackageName.equals(rhs.mPackageName)
                 && mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs)
+                && mRestrictedTransports.equals(rhs.mRestrictedTransports)
                 && mIsTestModeProfile == rhs.mIsTestModeProfile;
     }
 
@@ -189,12 +256,15 @@
         @NonNull
         private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs = new ArraySet<>();
 
+        @NonNull private final Set<Integer> mRestrictedTransports = new ArraySet<>();
+
         private boolean mIsTestModeProfile = false;
 
         public Builder(@NonNull Context context) {
             Objects.requireNonNull(context, "context was null");
 
             mPackageName = context.getOpPackageName();
+            mRestrictedTransports.addAll(RESTRICTED_TRANSPORTS_DEFAULT);
         }
 
         /**
@@ -225,6 +295,36 @@
             return this;
         }
 
+        private void validateRestrictedTransportsOrThrow(Set<Integer> restrictedTransports) {
+            Objects.requireNonNull(restrictedTransports, "transports was null");
+
+            for (int transport : restrictedTransports) {
+                if (!ALLOWED_TRANSPORTS.contains(transport)) {
+                    throw new IllegalArgumentException("Invalid transport " + transport);
+                }
+            }
+        }
+
+        /**
+         * Sets transports that will be restricted by the VCN.
+         *
+         * @param transports transports that will be restricted by VCN. Networks that include any
+         *     of the transports will be marked as restricted. Only {@link
+         *     NetworkCapabilities#TRANSPORT_WIFI} and {@link
+         *     NetworkCapabilities#TRANSPORT_CELLULAR} are allowed. {@link
+         *     NetworkCapabilities#TRANSPORT_WIFI} is marked restricted by default.
+         * @return this {@link Builder} instance, for chaining
+         * @throws IllegalArgumentException if the input contains unsupported transport types.
+         */
+        @NonNull
+        public Builder setRestrictedUnderlyingNetworkTransports(@NonNull Set<Integer> transports) {
+            validateRestrictedTransportsOrThrow(transports);
+
+            mRestrictedTransports.clear();
+            mRestrictedTransports.addAll(transports);
+            return this;
+        }
+
         /**
          * Restricts this VcnConfig to matching with test networks (only).
          *
@@ -248,7 +348,11 @@
          */
         @NonNull
         public VcnConfig build() {
-            return new VcnConfig(mPackageName, mGatewayConnectionConfigs, mIsTestModeProfile);
+            return new VcnConfig(
+                    mPackageName,
+                    mGatewayConnectionConfigs,
+                    mRestrictedTransports,
+                    mIsTestModeProfile);
         }
     }
 }
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index 2339656..b8850f4 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -42,6 +42,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -130,6 +131,30 @@
             })
     public @interface VcnSupportedCapability {}
 
+    /**
+     * Perform mobility update to attempt recovery from suspected data stalls.
+     *
+     * <p>If set, the gatway connection will monitor the data stall detection of the VCN network.
+     * When there is a suspected data stall, the gateway connection will attempt recovery by
+     * performing a mobility update on the underlying IKE session.
+     */
+    public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = {"VCN_GATEWAY_OPTION_"},
+            value = {
+                VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY,
+            })
+    public @interface VcnGatewayOption {}
+
+    private static final Set<Integer> ALLOWED_GATEWAY_OPTIONS = new ArraySet<>();
+
+    static {
+        ALLOWED_GATEWAY_OPTIONS.add(VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY);
+    }
+
     private static final int DEFAULT_MAX_MTU = 1500;
 
     /**
@@ -201,6 +226,9 @@
     private static final String RETRY_INTERVAL_MS_KEY = "mRetryIntervalsMs";
     @NonNull private final long[] mRetryIntervalsMs;
 
+    private static final String GATEWAY_OPTIONS_KEY = "mGatewayOptions";
+    @NonNull private final Set<Integer> mGatewayOptions;
+
     /** Builds a VcnGatewayConnectionConfig with the specified parameters. */
     private VcnGatewayConnectionConfig(
             @NonNull String gatewayConnectionName,
@@ -208,12 +236,14 @@
             @NonNull Set<Integer> exposedCapabilities,
             @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
             @NonNull long[] retryIntervalsMs,
-            @IntRange(from = MIN_MTU_V6) int maxMtu) {
+            @IntRange(from = MIN_MTU_V6) int maxMtu,
+            @NonNull Set<Integer> gatewayOptions) {
         mGatewayConnectionName = gatewayConnectionName;
         mTunnelConnectionParams = tunnelConnectionParams;
         mExposedCapabilities = new TreeSet(exposedCapabilities);
         mRetryIntervalsMs = retryIntervalsMs;
         mMaxMtu = maxMtu;
+        mGatewayOptions = Collections.unmodifiableSet(new HashSet(gatewayOptions));
 
         mUnderlyingNetworkTemplates = new ArrayList<>(underlyingNetworkTemplates);
         if (mUnderlyingNetworkTemplates.isEmpty()) {
@@ -256,6 +286,20 @@
                             VcnUnderlyingNetworkTemplate::fromPersistableBundle);
         }
 
+        final PersistableBundle gatewayOptionsBundle = in.getPersistableBundle(GATEWAY_OPTIONS_KEY);
+
+        if (gatewayOptionsBundle == null) {
+            // GATEWAY_OPTIONS_KEY was added in Android U. Thus VcnGatewayConnectionConfig created
+            // on old platforms will not have this data and will be assigned with the default value
+            mGatewayOptions = Collections.emptySet();
+        } else {
+            mGatewayOptions =
+                    new HashSet<>(
+                            PersistableBundleUtils.toList(
+                                    gatewayOptionsBundle,
+                                    PersistableBundleUtils.INTEGER_DESERIALIZER));
+        }
+
         mRetryIntervalsMs = in.getLongArray(RETRY_INTERVAL_MS_KEY);
         mMaxMtu = in.getInt(MAX_MTU_KEY);
 
@@ -279,6 +323,10 @@
 
         Preconditions.checkArgument(
                 mMaxMtu >= MIN_MTU_V6, "maxMtu must be at least IPv6 min MTU (1280)");
+
+        for (int option : mGatewayOptions) {
+            validateGatewayOption(option);
+        }
     }
 
     private static void checkValidCapability(int capability) {
@@ -315,6 +363,12 @@
         }
     }
 
+    private static void validateGatewayOption(int option) {
+        if (!ALLOWED_GATEWAY_OPTIONS.contains(option)) {
+            throw new IllegalArgumentException("Invalid vcn gateway option: " + option);
+        }
+    }
+
     /**
      * Returns the configured Gateway Connection name.
      *
@@ -399,6 +453,19 @@
     }
 
     /**
+     * Checks if the given VCN gateway option is enabled.
+     *
+     * @param option the option to check.
+     * @throws IllegalArgumentException if the provided option is invalid.
+     * @see Builder#addGatewayOption(int)
+     * @see Builder#removeGatewayOption(int)
+     */
+    public boolean hasGatewayOption(@VcnGatewayOption int option) {
+        validateGatewayOption(option);
+        return mGatewayOptions.contains(option);
+    }
+
+    /**
      * Converts this config to a PersistableBundle.
      *
      * @hide
@@ -418,11 +485,16 @@
                 PersistableBundleUtils.fromList(
                         mUnderlyingNetworkTemplates,
                         VcnUnderlyingNetworkTemplate::toPersistableBundle);
+        final PersistableBundle gatewayOptionsBundle =
+                PersistableBundleUtils.fromList(
+                        new ArrayList<>(mGatewayOptions),
+                        PersistableBundleUtils.INTEGER_SERIALIZER);
 
         result.putString(GATEWAY_CONNECTION_NAME_KEY, mGatewayConnectionName);
         result.putPersistableBundle(TUNNEL_CONNECTION_PARAMS_KEY, tunnelConnectionParamsBundle);
         result.putPersistableBundle(EXPOSED_CAPABILITIES_KEY, exposedCapsBundle);
         result.putPersistableBundle(UNDERLYING_NETWORK_TEMPLATES_KEY, networkTemplatesBundle);
+        result.putPersistableBundle(GATEWAY_OPTIONS_KEY, gatewayOptionsBundle);
         result.putLongArray(RETRY_INTERVAL_MS_KEY, mRetryIntervalsMs);
         result.putInt(MAX_MTU_KEY, mMaxMtu);
 
@@ -437,7 +509,8 @@
                 mExposedCapabilities,
                 mUnderlyingNetworkTemplates,
                 Arrays.hashCode(mRetryIntervalsMs),
-                mMaxMtu);
+                mMaxMtu,
+                mGatewayOptions);
     }
 
     @Override
@@ -452,7 +525,8 @@
                 && mExposedCapabilities.equals(rhs.mExposedCapabilities)
                 && mUnderlyingNetworkTemplates.equals(rhs.mUnderlyingNetworkTemplates)
                 && Arrays.equals(mRetryIntervalsMs, rhs.mRetryIntervalsMs)
-                && mMaxMtu == rhs.mMaxMtu;
+                && mMaxMtu == rhs.mMaxMtu
+                && mGatewayOptions.equals(rhs.mGatewayOptions);
     }
 
     /**
@@ -470,6 +544,8 @@
         @NonNull private long[] mRetryIntervalsMs = DEFAULT_RETRY_INTERVALS_MS;
         private int mMaxMtu = DEFAULT_MAX_MTU;
 
+        @NonNull private final Set<Integer> mGatewayOptions = new ArraySet<>();
+
         // TODO: (b/175829816) Consider VCN-exposed capabilities that may be transport dependent.
         //       Consider the case where the VCN might only expose MMS on WiFi, but defer to MMS
         //       when on Cell.
@@ -628,6 +704,34 @@
         }
 
         /**
+         * Enables the specified VCN gateway option.
+         *
+         * @param option the option to be enabled
+         * @return this {@link Builder} instance, for chaining
+         * @throws IllegalArgumentException if the provided option is invalid
+         */
+        @NonNull
+        public Builder addGatewayOption(@VcnGatewayOption int option) {
+            validateGatewayOption(option);
+            mGatewayOptions.add(option);
+            return this;
+        }
+
+        /**
+         * Resets (disables) the specified VCN gateway option.
+         *
+         * @param option the option to be disabled
+         * @return this {@link Builder} instance, for chaining
+         * @throws IllegalArgumentException if the provided option is invalid
+         */
+        @NonNull
+        public Builder removeGatewayOption(@VcnGatewayOption int option) {
+            validateGatewayOption(option);
+            mGatewayOptions.remove(option);
+            return this;
+        }
+
+        /**
          * Builds and validates the VcnGatewayConnectionConfig.
          *
          * @return an immutable VcnGatewayConnectionConfig instance
@@ -640,7 +744,8 @@
                     mExposedCapabilities,
                     mUnderlyingNetworkTemplates,
                     mRetryIntervalsMs,
-                    mMaxMtu);
+                    mMaxMtu,
+                    mGatewayOptions);
         }
     }
 }
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index f545c30..0c7f529 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -30,6 +30,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.IntentFilter;
+import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.nfc.tech.MifareClassic;
@@ -525,6 +526,66 @@
     }
 
     /**
+     * Helper to check if this device has FEATURE_NFC_BEAM, but without using
+     * a context.
+     * Equivalent to
+     * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC_BEAM)
+     */
+    private static boolean hasBeamFeature() {
+        IPackageManager pm = ActivityThread.getPackageManager();
+        if (pm == null) {
+            Log.e(TAG, "Cannot get package manager, assuming no Android Beam feature");
+            return false;
+        }
+        try {
+            return pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM, 0);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Package manager query failed, assuming no Android Beam feature", e);
+            return false;
+        }
+    }
+
+    /**
+     * Helper to check if this device has FEATURE_NFC, but without using
+     * a context.
+     * Equivalent to
+     * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)
+     */
+    private static boolean hasNfcFeature() {
+        IPackageManager pm = ActivityThread.getPackageManager();
+        if (pm == null) {
+            Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
+            return false;
+        }
+        try {
+            return pm.hasSystemFeature(PackageManager.FEATURE_NFC, 0);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
+            return false;
+        }
+    }
+
+    /**
+     * Helper to check if this device is NFC HCE capable, by checking for
+     * FEATURE_NFC_HOST_CARD_EMULATION and/or FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
+     * but without using a context.
+     */
+    private static boolean hasNfcHceFeature() {
+        IPackageManager pm = ActivityThread.getPackageManager();
+        if (pm == null) {
+            Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
+            return false;
+        }
+        try {
+            return pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)
+                || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
+            return false;
+        }
+    }
+
+    /**
      * Return list of Secure Elements which support off host card emulation.
      *
      * @return List<String> containing secure elements on the device which supports
@@ -533,21 +594,23 @@
      * @hide
      */
     public @NonNull List<String> getSupportedOffHostSecureElements() {
-        if (mContext == null) {
-            throw new UnsupportedOperationException("You need a context on NfcAdapter to use the "
-                    + " getSupportedOffHostSecureElements APIs");
-        }
         List<String> offHostSE = new ArrayList<String>();
-        PackageManager pm = mContext.getPackageManager();
+        IPackageManager pm = ActivityThread.getPackageManager();
         if (pm == null) {
             Log.e(TAG, "Cannot get package manager, assuming no off-host CE feature");
             return offHostSE;
         }
-        if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC)) {
-            offHostSE.add("SIM");
-        }
-        if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE)) {
-            offHostSE.add("eSE");
+        try {
+            if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC, 0)) {
+                offHostSE.add("SIM");
+            }
+            if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE, 0)) {
+                offHostSE.add("eSE");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Package manager query failed, assuming no off-host CE feature", e);
+            offHostSE.clear();
+            return offHostSE;
         }
         return offHostSE;
     }
@@ -559,19 +622,10 @@
      */
     @UnsupportedAppUsage
     public static synchronized NfcAdapter getNfcAdapter(Context context) {
-        if (context == null) {
-            if (sNullContextNfcAdapter == null) {
-                sNullContextNfcAdapter = new NfcAdapter(null);
-            }
-            return sNullContextNfcAdapter;
-        }
         if (!sIsInitialized) {
-            PackageManager pm = context.getPackageManager();
-            sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC);
-            sHasBeamFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM);
-            boolean hasHceFeature =
-                    pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)
-                    || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF);
+            sHasNfcFeature = hasNfcFeature();
+            sHasBeamFeature = hasBeamFeature();
+            boolean hasHceFeature = hasNfcHceFeature();
             /* is this device meant to have NFC */
             if (!sHasNfcFeature && !hasHceFeature) {
                 Log.v(TAG, "this device does not have NFC support");
@@ -607,6 +661,12 @@
 
             sIsInitialized = true;
         }
+        if (context == null) {
+            if (sNullContextNfcAdapter == null) {
+                sNullContextNfcAdapter = new NfcAdapter(null);
+            }
+            return sNullContextNfcAdapter;
+        }
         NfcAdapter adapter = sNfcAdapters.get(context);
         if (adapter == null) {
             adapter = new NfcAdapter(context);
@@ -617,12 +677,8 @@
 
     /** get handle to NFC service interface */
     private static INfcAdapter getServiceInterface() {
-        if (!sHasNfcFeature) {
-            /* NFC is not supported */
-            return null;
-        }
         /* get a handle to NFC service */
-        IBinder b = ServiceManager.waitForService("nfc");
+        IBinder b = ServiceManager.getService("nfc");
         if (b == null) {
             return null;
         }
@@ -652,15 +708,6 @@
                     "context not associated with any application (using a mock context?)");
         }
 
-        synchronized (NfcAdapter.class) {
-            if (!sIsInitialized) {
-                PackageManager pm = context.getPackageManager();
-                sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC);
-            }
-            if (!sHasNfcFeature) {
-                return null;
-            }
-        }
         if (getServiceInterface() == null) {
             // NFC is not available
             return null;
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index 6a42091..0b56d19 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -22,9 +22,11 @@
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.app.Activity;
+import android.app.ActivityThread;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.nfc.INfcCardEmulation;
 import android.nfc.NfcAdapter;
@@ -156,13 +158,18 @@
             throw new UnsupportedOperationException();
         }
         if (!sIsInitialized) {
-            PackageManager pm = context.getPackageManager();
+            IPackageManager pm = ActivityThread.getPackageManager();
             if (pm == null) {
                 Log.e(TAG, "Cannot get PackageManager");
                 throw new UnsupportedOperationException();
             }
-            if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) {
-                Log.e(TAG, "This device does not support card emulation");
+            try {
+                if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)) {
+                    Log.e(TAG, "This device does not support card emulation");
+                    throw new UnsupportedOperationException();
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "PackageManager query failed.");
                 throw new UnsupportedOperationException();
             }
             sIsInitialized = true;
diff --git a/core/java/android/nfc/cardemulation/NfcFCardEmulation.java b/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
index 48bbf5b6..3c92455 100644
--- a/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
+++ b/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
@@ -17,8 +17,10 @@
 package android.nfc.cardemulation;
 
 import android.app.Activity;
+import android.app.ActivityThread;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.nfc.INfcFCardEmulation;
 import android.nfc.NfcAdapter;
@@ -68,13 +70,18 @@
             throw new UnsupportedOperationException();
         }
         if (!sIsInitialized) {
-            PackageManager pm = context.getPackageManager();
+            IPackageManager pm = ActivityThread.getPackageManager();
             if (pm == null) {
                 Log.e(TAG, "Cannot get PackageManager");
                 throw new UnsupportedOperationException();
             }
-            if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF)) {
-                Log.e(TAG, "This device does not support NFC-F card emulation");
+            try {
+                if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0)) {
+                    Log.e(TAG, "This device does not support NFC-F card emulation");
+                    throw new UnsupportedOperationException();
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "PackageManager query failed.");
                 throw new UnsupportedOperationException();
             }
             sIsInitialized = true;
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index e915d98..2a4c861 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -44,6 +44,7 @@
 import android.util.MutableBoolean;
 import android.util.Pair;
 import android.util.Printer;
+import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseDoubleArray;
 import android.util.SparseIntArray;
@@ -52,6 +53,7 @@
 import android.view.Display;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BatteryStatsHistoryIterator;
 
 import com.google.android.collect.Lists;
 
@@ -2398,9 +2400,6 @@
 
     public abstract int getHistoryUsedSize();
 
-    @UnsupportedAppUsage
-    public abstract boolean startIteratingHistoryLocked();
-
     public abstract int getHistoryStringPoolSize();
 
     public abstract int getHistoryStringPoolBytes();
@@ -2409,10 +2408,11 @@
 
     public abstract int getHistoryTagPoolUid(int index);
 
-    @UnsupportedAppUsage
-    public abstract boolean getNextHistoryLocked(HistoryItem out);
-
-    public abstract void finishIteratingHistoryLocked();
+    /**
+     * Returns a BatteryStatsHistoryIterator. Battery history will remain immutable until the
+     * {@link BatteryStatsHistoryIterator#close()} method is invoked.
+     */
+    public abstract BatteryStatsHistoryIterator iterateBatteryStatsHistory();
 
     /**
      * Returns the number of times the device has been started.
@@ -7451,80 +7451,88 @@
 
     private void dumpHistoryLocked(PrintWriter pw, int flags, long histStart, boolean checkin) {
         final HistoryPrinter hprinter = new HistoryPrinter();
-        final HistoryItem rec = new HistoryItem();
         long lastTime = -1;
         long baseTime = -1;
         boolean printed = false;
         HistoryEventTracker tracker = null;
-        while (getNextHistoryLocked(rec)) {
-            lastTime = rec.time;
-            if (baseTime < 0) {
-                baseTime = lastTime;
-            }
-            if (rec.time >= histStart) {
-                if (histStart >= 0 && !printed) {
-                    if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
-                            || rec.cmd == HistoryItem.CMD_RESET
-                            || rec.cmd == HistoryItem.CMD_START
-                            || rec.cmd == HistoryItem.CMD_SHUTDOWN) {
-                        printed = true;
-                        hprinter.printNextItem(pw, rec, baseTime, checkin,
-                                (flags&DUMP_VERBOSE) != 0);
-                        rec.cmd = HistoryItem.CMD_UPDATE;
-                    } else if (rec.currentTime != 0) {
-                        printed = true;
-                        byte cmd = rec.cmd;
-                        rec.cmd = HistoryItem.CMD_CURRENT_TIME;
-                        hprinter.printNextItem(pw, rec, baseTime, checkin,
-                                (flags&DUMP_VERBOSE) != 0);
-                        rec.cmd = cmd;
+        try (BatteryStatsHistoryIterator iterator = iterateBatteryStatsHistory()) {
+            HistoryItem rec;
+            while ((rec = iterator.next()) != null) {
+                try {
+                    lastTime = rec.time;
+                    if (baseTime < 0) {
+                        baseTime = lastTime;
                     }
-                    if (tracker != null) {
-                        if (rec.cmd != HistoryItem.CMD_UPDATE) {
-                            hprinter.printNextItem(pw, rec, baseTime, checkin,
-                                    (flags&DUMP_VERBOSE) != 0);
-                            rec.cmd = HistoryItem.CMD_UPDATE;
-                        }
-                        int oldEventCode = rec.eventCode;
-                        HistoryTag oldEventTag = rec.eventTag;
-                        rec.eventTag = new HistoryTag();
-                        for (int i=0; i<HistoryItem.EVENT_COUNT; i++) {
-                            HashMap<String, SparseIntArray> active
-                                    = tracker.getStateForEvent(i);
-                            if (active == null) {
-                                continue;
+                    if (rec.time >= histStart) {
+                        if (histStart >= 0 && !printed) {
+                            if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
+                                    || rec.cmd == HistoryItem.CMD_RESET
+                                    || rec.cmd == HistoryItem.CMD_START
+                                    || rec.cmd == HistoryItem.CMD_SHUTDOWN) {
+                                printed = true;
+                                hprinter.printNextItem(pw, rec, baseTime, checkin,
+                                        (flags & DUMP_VERBOSE) != 0);
+                                rec.cmd = HistoryItem.CMD_UPDATE;
+                            } else if (rec.currentTime != 0) {
+                                printed = true;
+                                byte cmd = rec.cmd;
+                                rec.cmd = HistoryItem.CMD_CURRENT_TIME;
+                                hprinter.printNextItem(pw, rec, baseTime, checkin,
+                                        (flags & DUMP_VERBOSE) != 0);
+                                rec.cmd = cmd;
                             }
-                            for (HashMap.Entry<String, SparseIntArray> ent
-                                    : active.entrySet()) {
-                                SparseIntArray uids = ent.getValue();
-                                for (int j=0; j<uids.size(); j++) {
-                                    rec.eventCode = i;
-                                    rec.eventTag.string = ent.getKey();
-                                    rec.eventTag.uid = uids.keyAt(j);
-                                    rec.eventTag.poolIdx = uids.valueAt(j);
+                            if (tracker != null) {
+                                if (rec.cmd != HistoryItem.CMD_UPDATE) {
                                     hprinter.printNextItem(pw, rec, baseTime, checkin,
-                                            (flags&DUMP_VERBOSE) != 0);
-                                    rec.wakeReasonTag = null;
-                                    rec.wakelockTag = null;
+                                            (flags & DUMP_VERBOSE) != 0);
+                                    rec.cmd = HistoryItem.CMD_UPDATE;
                                 }
+                                int oldEventCode = rec.eventCode;
+                                HistoryTag oldEventTag = rec.eventTag;
+                                rec.eventTag = new HistoryTag();
+                                for (int i = 0; i < HistoryItem.EVENT_COUNT; i++) {
+                                    Map<String, SparseIntArray> active =
+                                            tracker.getStateForEvent(i);
+                                    if (active == null) {
+                                        continue;
+                                    }
+                                    for (Map.Entry<String, SparseIntArray> ent :
+                                            active.entrySet()) {
+                                        SparseIntArray uids = ent.getValue();
+                                        for (int j = 0; j < uids.size(); j++) {
+                                            rec.eventCode = i;
+                                            rec.eventTag.string = ent.getKey();
+                                            rec.eventTag.uid = uids.keyAt(j);
+                                            rec.eventTag.poolIdx = uids.valueAt(j);
+                                            hprinter.printNextItem(pw, rec, baseTime, checkin,
+                                                    (flags & DUMP_VERBOSE) != 0);
+                                            rec.wakeReasonTag = null;
+                                            rec.wakelockTag = null;
+                                        }
+                                    }
+                                }
+                                rec.eventCode = oldEventCode;
+                                rec.eventTag = oldEventTag;
+                                tracker = null;
                             }
                         }
-                        rec.eventCode = oldEventCode;
-                        rec.eventTag = oldEventTag;
-                        tracker = null;
+                        hprinter.printNextItem(pw, rec, baseTime, checkin,
+                                (flags & DUMP_VERBOSE) != 0);
+                    } else if (false/* && rec.eventCode != HistoryItem.EVENT_NONE */) {
+                        // This is an attempt to aggregate the previous state and generate
+                        // fake events to reflect that state at the point where we start
+                        // printing real events.  It doesn't really work right, so is turned off.
+                        if (tracker == null) {
+                            tracker = new HistoryEventTracker();
+                        }
+                        tracker.updateState(rec.eventCode, rec.eventTag.string,
+                                rec.eventTag.uid, rec.eventTag.poolIdx);
                     }
+                } catch (Throwable t) {
+                    t.printStackTrace(pw);
+                    Slog.wtf(TAG, "Corrupted battery history", t);
+                    break;
                 }
-                hprinter.printNextItem(pw, rec, baseTime, checkin,
-                        (flags&DUMP_VERBOSE) != 0);
-            } else if (false && rec.eventCode != HistoryItem.EVENT_NONE) {
-                // This is an attempt to aggregate the previous state and generate
-                // fake events to reflect that state at the point where we start
-                // printing real events.  It doesn't really work right, so is turned off.
-                if (tracker == null) {
-                    tracker = new HistoryEventTracker();
-                }
-                tracker.updateState(rec.eventCode, rec.eventTag.string,
-                        rec.eventTag.uid, rec.eventTag.poolIdx);
             }
         }
         if (histStart >= 0) {
@@ -7595,25 +7603,19 @@
         if ((flags&DUMP_HISTORY_ONLY) != 0 || !filtering) {
             final long historyTotalSize = getHistoryTotalSize();
             final long historyUsedSize = getHistoryUsedSize();
-            if (startIteratingHistoryLocked()) {
-                try {
-                    pw.print("Battery History (");
-                    pw.print((100*historyUsedSize)/historyTotalSize);
-                    pw.print("% used, ");
-                    printSizeValue(pw, historyUsedSize);
-                    pw.print(" used of ");
-                    printSizeValue(pw, historyTotalSize);
-                    pw.print(", ");
-                    pw.print(getHistoryStringPoolSize());
-                    pw.print(" strings using ");
-                    printSizeValue(pw, getHistoryStringPoolBytes());
-                    pw.println("):");
-                    dumpHistoryLocked(pw, flags, histStart, false);
-                    pw.println();
-                } finally {
-                    finishIteratingHistoryLocked();
-                }
-            }
+            pw.print("Battery History (");
+            pw.print((100 * historyUsedSize) / historyTotalSize);
+            pw.print("% used, ");
+            printSizeValue(pw, historyUsedSize);
+            pw.print(" used of ");
+            printSizeValue(pw, historyTotalSize);
+            pw.print(", ");
+            pw.print(getHistoryStringPoolSize());
+            pw.print(" strings using ");
+            printSizeValue(pw, getHistoryStringPoolBytes());
+            pw.println("):");
+            dumpHistoryLocked(pw, flags, histStart, false);
+            pw.println();
         }
 
         if (filtering && (flags&(DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) == 0) {
@@ -7770,28 +7772,24 @@
                 getEndPlatformVersion());
 
         if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) {
-            if (startIteratingHistoryLocked()) {
-                try {
-                    for (int i=0; i<getHistoryStringPoolSize(); i++) {
-                        pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(',');
-                        pw.print(HISTORY_STRING_POOL); pw.print(',');
-                        pw.print(i);
-                        pw.print(",");
-                        pw.print(getHistoryTagPoolUid(i));
-                        pw.print(",\"");
-                        String str = getHistoryTagPoolString(i);
-                        if (str != null) {
-                            str = str.replace("\\", "\\\\");
-                            str = str.replace("\"", "\\\"");
-                            pw.print(str);
-                        }
-                        pw.print("\"");
-                        pw.println();
-                    }
-                    dumpHistoryLocked(pw, flags, histStart, true);
-                } finally {
-                    finishIteratingHistoryLocked();
+            for (int i = 0; i < getHistoryStringPoolSize(); i++) {
+                pw.print(BATTERY_STATS_CHECKIN_VERSION);
+                pw.print(',');
+                pw.print(HISTORY_STRING_POOL);
+                pw.print(',');
+                pw.print(i);
+                pw.print(",");
+                pw.print(getHistoryTagPoolUid(i));
+                pw.print(",\"");
+                String str = getHistoryTagPoolString(i);
+                if (str != null) {
+                    str = str.replace("\\", "\\\\");
+                    str = str.replace("\"", "\\\"");
+                    pw.print(str);
                 }
+                pw.print("\"");
+                pw.println();
+                dumpHistoryLocked(pw, flags, histStart, true);
             }
         }
 
@@ -8331,17 +8329,12 @@
     }
 
     private void dumpProtoHistoryLocked(ProtoOutputStream proto, int flags, long histStart) {
-        if (!startIteratingHistoryLocked()) {
-            return;
-        }
-
         proto.write(BatteryStatsServiceDumpHistoryProto.REPORT_VERSION, CHECKIN_VERSION);
         proto.write(BatteryStatsServiceDumpHistoryProto.PARCEL_VERSION, getParcelVersion());
         proto.write(BatteryStatsServiceDumpHistoryProto.START_PLATFORM_VERSION,
                 getStartPlatformVersion());
         proto.write(BatteryStatsServiceDumpHistoryProto.END_PLATFORM_VERSION,
                 getEndPlatformVersion());
-        try {
             long token;
             // History string pool (HISTORY_STRING_POOL)
             for (int i = 0; i < getHistoryStringPoolSize(); ++i) {
@@ -8353,14 +8346,15 @@
                 proto.end(token);
             }
 
-            // History data (HISTORY_DATA)
-            final HistoryPrinter hprinter = new HistoryPrinter();
-            final HistoryItem rec = new HistoryItem();
-            long lastTime = -1;
-            long baseTime = -1;
-            boolean printed = false;
-            HistoryEventTracker tracker = null;
-            while (getNextHistoryLocked(rec)) {
+        // History data (HISTORY_DATA)
+        final HistoryPrinter hprinter = new HistoryPrinter();
+        long lastTime = -1;
+        long baseTime = -1;
+        boolean printed = false;
+        HistoryEventTracker tracker = null;
+        try (BatteryStatsHistoryIterator iterator = iterateBatteryStatsHistory()) {
+            HistoryItem rec;
+            while ((rec = iterator.next()) != null) {
                 lastTime = rec.time;
                 if (baseTime < 0) {
                     baseTime = lastTime;
@@ -8427,8 +8421,6 @@
                 proto.write(BatteryStatsServiceDumpHistoryProto.CSV_LINES,
                         "NEXT: " + (lastTime + 1));
             }
-        } finally {
-            finishIteratingHistoryLocked();
         }
     }
 
diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java
index a529ac6..712d328 100644
--- a/core/java/android/os/Looper.java
+++ b/core/java/android/os/Looper.java
@@ -177,12 +177,15 @@
         final long traceTag = me.mTraceTag;
         long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
         long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
-        if (thresholdOverride > 0) {
+
+        final boolean hasOverride = thresholdOverride >= 0;
+        if (hasOverride) {
             slowDispatchThresholdMs = thresholdOverride;
             slowDeliveryThresholdMs = thresholdOverride;
         }
-        final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
-        final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
+        final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0 || hasOverride)
+                && (msg.when > 0);
+        final boolean logSlowDispatch = (slowDispatchThresholdMs > 0 || hasOverride);
 
         final boolean needStartTime = logSlowDelivery || logSlowDispatch;
         final boolean needEndTime = logSlowDispatch;
@@ -283,7 +286,7 @@
                 SystemProperties.getInt("log.looper."
                         + Process.myUid() + "."
                         + Thread.currentThread().getName()
-                        + ".slow", 0);
+                        + ".slow", -1);
 
         me.mSlowDeliveryDetected = false;
 
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index d50ba8d..363d035 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -435,25 +435,27 @@
                 Uri.withAppendedPath(AUTHORITY_URI, "directories");
 
         /**
-         * URI used for getting all directories from primary and managed profile.
-         * It supports the same semantics as {@link #CONTENT_URI} and returns the same columns.
-         * If the device has no managed profile that is linked to the current profile, it behaves
-         * in the exact same way as {@link #CONTENT_URI}.
-         * If there is a managed profile linked to the current profile, it will merge
-         * managed profile and current profile's results and return.
-         *
-         * Note: this query returns primary profile results before managed profile results,
-         * and this order is not affected by sorting parameter.
+         * URI used for getting all directories from both the calling user and the managed profile
+         * that is linked to it.
+         * <p>
+         * It supports the same semantics as {@link #CONTENT_URI} and returns the same columns.<br>
+         * If the device has no managed profile that is linked to the calling user, it behaves
+         * in the exact same way as {@link #CONTENT_URI}.<br>
+         * If there is a managed profile linked to the calling user, it will return merged results
+         * from both.
+         * <p>
+         * Note: this query returns the calling user results before the managed profile results,
+         * and this order is not affected by the sorting parameter.
          *
          */
         public static final Uri ENTERPRISE_CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI,
                 "directories_enterprise");
 
         /**
-         * Access file provided by remote directory. It allows both personal and work remote
-         * directory, but not local and invisible diretory.
-         *
-         * It's supported only by a few specific places for referring to contact pictures in the
+         * Access file provided by remote directory. It allows both calling user and managed profile
+         * remote directory, but not local and invisible directory.
+         * <p>
+         * It is supported only by a few specific places for referring to contact pictures in the
          * remote directory. Contact picture URIs, e.g.
          * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}, may contain this kind of URI.
          *
@@ -490,13 +492,13 @@
         public static final long LOCAL_INVISIBLE = 1;
 
         /**
-         * _ID of the work profile default directory, which represents locally stored contacts.
+         * _ID of the managed profile default directory, which represents locally stored contacts.
          */
         public static final long ENTERPRISE_DEFAULT = Directory.ENTERPRISE_DIRECTORY_ID_BASE
                 + DEFAULT;
 
         /**
-         * _ID of the work profile directory that represents locally stored invisible contacts.
+         * _ID of the managed profile directory that represents locally stored invisible contacts.
          */
         public static final long ENTERPRISE_LOCAL_INVISIBLE = Directory.ENTERPRISE_DIRECTORY_ID_BASE
                 + LOCAL_INVISIBLE;
@@ -557,8 +559,8 @@
         public static final String ACCOUNT_NAME = "accountName";
 
         /**
-         * Mimimal ID for corp directory returned from
-         * {@link Directory#CORP_CONTENT_URI}.
+         * Mimimal ID for managed profile directory returned from
+         * {@link Directory#ENTERPRISE_CONTENT_URI}.
          *
          * @hide
          */
@@ -1537,12 +1539,42 @@
         public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "contacts");
 
         /**
-         * Special contacts URI to refer to contacts on the corp profile from the personal
-         * profile.
-         *
+         * URI used for getting all contacts from both the calling user and the managed profile
+         * that is linked to it.
+         * <p>
+         * It supports the same semantics as {@link #CONTENT_URI} and returns the same columns.<br>
+         * If the calling user has no managed profile, it behaves in the exact same way as
+         * {@link #CONTENT_URI}.<br>
+         * If there is a managed profile linked to the calling user, it will return merged results
+         * from both.
+         * <p>
+         * Note: this query returns the calling user results before the managed profile results,
+         * and this order is not affected by the sorting parameter.
+         * <p>
+         * If a result is from the managed profile, the following changes are made to the data:
+         * <ul>
+         *     <li>{@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special
+         *     URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to
+         *     load pictures from them.
+         *     <li>{@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Don't use them.
+         *     <li>{@link #_ID} and {@link #LOOKUP_KEY} will be replaced with artificial values.
+         *     These values will be consistent across multiple queries, but do not use them in
+         *     places that do not explicitly say they accept them. If they are used in the
+         *     {@code selection} param in {@link android.content.ContentProvider#query}, the result
+         *     is undefined.
+         *     <li>In order to tell whether a contact is from the managed profile, use
+         *     {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
+         */
+        public static final @NonNull Uri ENTERPRISE_CONTENT_URI = Uri.withAppendedPath(
+                CONTENT_URI, "enterprise");
+
+        /**
+         * Special contacts URI to refer to contacts on the managed profile from the calling user.
+         * <p>
          * It's supported only by a few specific places for referring to contact pictures that
-         * are in the corp provider for enterprise caller-ID.  Contact picture URIs returned from
-         * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} may contain this kind of URI.
+         * are in the managed profile provider for enterprise caller-ID. Contact picture URIs
+         * returned from {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} and similar APIs may
+         * contain this kind of URI.
          *
          * @hide
          */
@@ -1736,7 +1768,8 @@
         /**
          * It supports the similar semantics as {@link #CONTENT_FILTER_URI} and returns the same
          * columns. This URI requires {@link ContactsContract#DIRECTORY_PARAM_KEY} in parameters,
-         * otherwise it will throw IllegalArgumentException.
+         * otherwise it will throw IllegalArgumentException. The passed directory can belong either
+         * to the calling user or to a managed profile that is linked to it.
          */
         public static final Uri ENTERPRISE_CONTENT_FILTER_URI = Uri.withAppendedPath(
                 CONTENT_URI, "filter_enterprise");
@@ -1807,25 +1840,25 @@
         public static final String CONTENT_VCARD_TYPE = "text/x-vcard";
 
         /**
-         * Mimimal ID for corp contacts returned from
-         * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
+         * Mimimal ID for managed profile contacts returned from
+         * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} and similar APIs
          *
          * @hide
          */
         public static long ENTERPRISE_CONTACT_ID_BASE = 1000000000; // slightly smaller than 2 ** 30
 
         /**
-         * Prefix for corp contacts returned from
-         * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
+         * Prefix for managed profile contacts returned from
+         * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} and similar APIs.
          *
          * @hide
          */
         public static String ENTERPRISE_CONTACT_LOOKUP_PREFIX = "c-";
 
         /**
-         * Return TRUE if a contact ID is from the contacts provider on the enterprise profile.
+         * Return {@code true} if a contact ID is from the contacts provider on the managed profile.
          *
-         * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} may return such a contact.
+         * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} and similar APIs may return such IDs.
          */
         public static boolean isEnterpriseContactId(long contactId) {
             return (contactId >= ENTERPRISE_CONTACT_ID_BASE) && (contactId < Profile.MIN_ID);
@@ -5167,7 +5200,7 @@
                 Uri.withAppendedPath(AUTHORITY_URI, "raw_contact_entities");
 
         /**
-        * The content:// style URI for this table in corp profile
+        * The content:// style URI for this table in the managed profile
         *
         * @hide
         */
@@ -5209,13 +5242,13 @@
         public static final String DATA_ID = "data_id";
 
         /**
-         * Query raw contacts entity by a contact ID, which can potentially be a corp profile
-         * contact ID
+         * Query raw contacts entity by a contact ID, which can potentially be a managed profile
+         * contact ID.
+         * <p>
+         * @param contentResolver The content resolver to query
+         * @param contactId Contact ID, which can potentially be a managed profile contact ID.
+         * @return A map from a mimetype to a list of the entity content values.
          *
-         * @param context A context to get the ContentResolver from
-         * @param contactId Contact ID, which can potentialy be a corp profile contact ID.
-         *
-         * @return A map from a mimetype to a List of the entity content values.
          * {@hide}
          */
         @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -5452,55 +5485,44 @@
                 "phone_lookup");
 
         /**
-         * <p>URI used for the "enterprise caller-id".</p>
-         *
-         * <p class="caution"><b>Caution: </b>If you publish your app to the Google Play Store, this
-         * field doesn't sort results based on contacts frequency. For more information, see the
-         * <a href="/guide/topics/providers/contacts-provider#ObsoleteData">Contacts Provider</a>
-         * page.
-         *
+         * URI used for looking up contacts by phone number on the contact databases of both the
+         * calling user and the managed profile that is linked to it.
          * <p>
          * It supports the same semantics as {@link #CONTENT_FILTER_URI} and returns the same
-         * columns.  If the device has no corp profile that is linked to the current profile, it
-         * behaves in the exact same way as {@link #CONTENT_FILTER_URI}.  If there is a corp profile
-         * linked to the current profile, it first queries against the personal contact database,
-         * and if no matching contacts are found there, then queries against the
-         * corp contacts database.
-         * </p>
+         * columns.<br>
+         * If the device has no managed profile that is linked to the calling user, it behaves in
+         * the exact same way as {@link #CONTENT_FILTER_URI}.<br>
+         * If there is a managed profile linked to the calling user, it first queries the calling
+         * user's contact database, and only if no matching contacts are found there it then queries
+         * the managed profile database.
+         * <p class="caution">
+         * <b>Caution: </b>If you publish your app to the Google Play Store, this field doesn't sort
+         * results based on contacts frequency. For more information, see the
+         * <a href="/guide/topics/providers/contacts-provider#ObsoleteData">Contacts Provider</a>
+         * page.
          * <p>
-         * If a result is from the corp profile, it makes the following changes to the data:
+         * If a result is from the managed profile, the following changes are made to the data:
          * <ul>
-         *     <li>
-         *     {@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special
-         *     URIs.  Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to
+         *     <li>{@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special
+         *     URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to
          *     load pictures from them.
-         *     {@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null.  Do not use them.
-         *     </li>
-         *     <li>
-         *     Corp contacts will get artificial {@link #_ID}s.  In order to tell whether a contact
-         *     is from the corp profile, use
+         *     <li>{@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Don't use them.
+         *     <li>{@link #CONTACT_ID} and {@link #LOOKUP_KEY} will be replaced with artificial
+         *     values. These values will be consistent across multiple queries, but do not use them
+         *     in places that do not explicitly say they accept them. If they are used in the
+         *     {@code selection} param in {@link android.content.ContentProvider#query}, the result
+         *     is undefined.
+         *     <li>In order to tell whether a contact is from the managed profile, use
          *     {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
-         *     </li>
-         *     <li>
-         *     Corp contacts will get artificial {@link #LOOKUP_KEY}s too.
-         *     </li>
-         *     <li>
-         *     Returned work contact IDs and lookup keys are not accepted in places that not
-         *     explicitly say to accept them.
-         *     </li>
-         * </ul>
          * <p>
          * A contact lookup URL built by
          * {@link ContactsContract.Contacts#getLookupUri(long, String)}
-         * with an {@link #_ID} and a {@link #LOOKUP_KEY} returned by this API can be passed to
-         * {@link ContactsContract.QuickContact#showQuickContact} even if a contact is from the
-         * corp profile.
-         * </p>
-         *
+         * with a {@link #CONTACT_ID} and a {@link #LOOKUP_KEY} returned by this API can be passed
+         * to {@link ContactsContract.QuickContact#showQuickContact} even if a contact is from the
+         * managed profile.
          * <pre>
          * Uri lookupUri = Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,
          *         Uri.encode(phoneNumber));
-         * </pre>
          */
         public static final Uri ENTERPRISE_CONTENT_FILTER_URI = Uri.withAppendedPath(AUTHORITY_URI,
                 "phone_lookup_enterprise");
@@ -6236,19 +6258,32 @@
                     "phones");
 
             /**
-            * URI used for getting all contacts from primary and managed profile.
-            *
-            * It supports the same semantics as {@link #CONTENT_URI} and returns the same
-            * columns.  If the device has no corp profile that is linked to the current profile, it
-            * behaves in the exact same way as {@link #CONTENT_URI}.  If there is a corp profile
-            * linked to the current profile, it will merge corp profile and current profile's
-            * results and return
-            *
-            * @hide
-            */
-            @SystemApi
-            @TestApi
-            @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)
+             * URI used for getting all data records of the {@link #CONTENT_ITEM_TYPE} MIME type,
+             * combined with the associated raw contact and aggregate contact data, from both the
+             * calling user and the managed profile that is linked to it.
+             * <p>
+             * It supports the same semantics as {@link #CONTENT_URI} and returns the same
+             * columns.<br>
+             * If the device has no managed profile that is linked to the calling user, it behaves
+             * in the exact same way as {@link #CONTENT_URI}.<br>
+             * If there is a managed profile linked to the calling user, it will return merged
+             * results from both.
+             * <p>
+             * If a result is from the managed profile, the following changes are made to the data:
+             * <ul>
+             *     <li>{@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to
+             *     special URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings
+             *     to load pictures from them.
+             *     <li>{@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Don't use
+             *     them.
+             *     <li>{@link #CONTACT_ID} and {@link #LOOKUP_KEY} will be replaced with artificial
+             *     values. These values will be consistent across multiple queries, but do not use
+             *     them in places that don't explicitly say they accept them. If they are used in
+             *     the {@code selection} param in {@link android.content.ContentProvider#query}, the
+             *     result is undefined.
+             *     <li>In order to tell whether a contact is from the managed profile, use
+             *     {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
+             */
             public static final @NonNull Uri ENTERPRISE_CONTENT_URI =
                     Uri.withAppendedPath(Data.ENTERPRISE_CONTENT_URI, "phones");
 
@@ -6483,53 +6518,41 @@
                     "lookup");
 
             /**
-            * <p>URI used for enterprise email lookup.</p>
-            *
-            * <p>
-            * It supports the same semantics as {@link #CONTENT_LOOKUP_URI} and returns the same
-            * columns.  If the device has no corp profile that is linked to the current profile, it
-            * behaves in the exact same way as {@link #CONTENT_LOOKUP_URI}.  If there is a
-            * corp profile linked to the current profile, it first queries against the personal contact database,
-            * and if no matching contacts are found there, then queries against the
-            * corp contacts database.
-            * </p>
-            * <p>
-            * If a result is from the corp profile, it makes the following changes to the data:
-            * <ul>
-            *     <li>
-            *     {@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special
-            *     URIs.  Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to
-            *     load pictures from them.
-            *     {@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null.  Do not
-            *     use them.
-            *     </li>
-            *     <li>
-            *     Corp contacts will get artificial {@link #CONTACT_ID}s.  In order to tell whether
-            *     a contact
-            *     is from the corp profile, use
-            *     {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
-             *     </li>
-             *     <li>
-             *     Corp contacts will get artificial {@link #LOOKUP_KEY}s too.
-             *     </li>
-             *     <li>
-             *     Returned work contact IDs and lookup keys are not accepted in places that not
-             *     explicitly say to accept them.
-             *     </li>
-             * </ul>
+             * URI used for looking up contacts by email on the contact databases of both the
+             * calling user and the managed profile that is linked to it.
+             * <p>
+             * It supports the same semantics as {@link #CONTENT_LOOKUP_URI} and returns the same
+             * columns.<br>
+             * If the device has no managed profile that is linked to the calling user, it behaves
+             * in the exact same way as {@link #CONTENT_LOOKUP_URI}.<br>
+             * If there is a managed profile linked to the calling user, it first queries the
+             * calling user's contact database, and only if no matching contacts are found there it
+             * then queries the managed profile database.
+             * <p class="caution">
+             * If a result is from the managed profile, the following changes are made to the data:
+             * <ul>
+             *     <li>{@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to
+             *     specialURIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings
+             *     to load pictures from them.
+             *     <li>{@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Don't use
+             *     them.
+             *     <li>{@link #CONTACT_ID} and {@link #LOOKUP_KEY} will be replaced with artificial
+             *     values. These values will be consistent across multiple queries, but do not use
+             *     them in places that do not explicitly say they accept them. If they are used in
+             *     the {@code selection} param in {@link android.content.ContentProvider#query}, the
+             *     result is undefined.
+             *     <li>In order to tell whether a contact is from the managed profile, use
+             *     {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
              * <p>
              * A contact lookup URL built by
              * {@link ContactsContract.Contacts#getLookupUri(long, String)}
-             * with an {@link #_ID} and a {@link #LOOKUP_KEY} returned by this API can be passed to
-             * {@link ContactsContract.QuickContact#showQuickContact} even if a contact is from the
-             * corp profile.
-             * </p>
-            *
-            * <pre>
-            * Uri lookupUri = Uri.withAppendedPath(Email.ENTERPRISE_CONTENT_LOOKUP_URI,
-            *         Uri.encode(email));
-            * </pre>
-            */
+             * with a {@link #CONTACT_ID} and a {@link #LOOKUP_KEY} returned by this API can be
+             * passed to {@link ContactsContract.QuickContact#showQuickContact} even if a contact is
+             * from the managed profile.
+             * <pre>
+             * Uri lookupUri = Uri.withAppendedPath(Email.ENTERPRISE_CONTENT_LOOKUP_URI,
+             *         Uri.encode(email));
+             */
             public static final Uri ENTERPRISE_CONTENT_LOOKUP_URI =
                     Uri.withAppendedPath(CONTENT_URI, "lookup_enterprise");
 
@@ -6562,9 +6585,10 @@
             /**
              * <p>It supports the similar semantics as {@link #CONTENT_FILTER_URI} and returns the
              * same columns. This URI requires {@link ContactsContract#DIRECTORY_PARAM_KEY} in
-             * parameters, otherwise it will throw IllegalArgumentException.
-             *
-             * <p class="caution"><b>Caution: </b>If you publish your app to the Google Play Store,
+             * parameters, otherwise it will throw IllegalArgumentException. The passed directory
+             * can belong either to the calling user or to a managed profile that is linked to it.
+             * <p class="caution">
+             * <b>Caution: </b>If you publish your app to the Google Play Store,
              * this field doesn't sort results based on contacts frequency. For more information,
              * see the
              * <a href="/guide/topics/providers/contacts-provider#ObsoleteData">Contacts Provider</a>
@@ -9261,7 +9285,7 @@
          *            around this {@link View}.
          * @param lookupUri A {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style
          *            {@link Uri} that describes a specific contact to feature
-         *            in this dialog. A work lookup uri is supported here,
+         *            in this dialog. A managed profile lookup uri is supported here,
          *            see {@link CommonDataKinds.Email#ENTERPRISE_CONTENT_LOOKUP_URI} and
          *            {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
          * @param mode Any of {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or
@@ -9297,7 +9321,7 @@
          * @param lookupUri A
          *            {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style
          *            {@link Uri} that describes a specific contact to feature
-         *            in this dialog. A work lookup uri is supported here,
+         *            in this dialog. A managed profile lookup uri is supported here,
          *            see {@link CommonDataKinds.Email#ENTERPRISE_CONTENT_LOOKUP_URI} and
          *            {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
          * @param mode Any of {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or
@@ -9330,7 +9354,7 @@
          * @param lookupUri A
          *            {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style
          *            {@link Uri} that describes a specific contact to feature
-         *            in this dialog. A work lookup uri is supported here,
+         *            in this dialog. A managed profile lookup uri is supported here,
          *            see {@link CommonDataKinds.Email#ENTERPRISE_CONTENT_LOOKUP_URI} and
          *            {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
          * @param excludeMimes Optional list of {@link Data#MIMETYPE} MIME-types
@@ -9370,7 +9394,7 @@
          * @param lookupUri A
          *            {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style
          *            {@link Uri} that describes a specific contact to feature
-         *            in this dialog. A work lookup uri is supported here,
+         *            in this dialog. A managed profile lookup uri is supported here,
          *            see {@link CommonDataKinds.Email#ENTERPRISE_CONTENT_LOOKUP_URI} and
          *            {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
          * @param excludeMimes Optional list of {@link Data#MIMETYPE} MIME-types
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e90ba6e..25f15d8 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6210,6 +6210,7 @@
             MOVED_TO_GLOBAL.add(Settings.Global.DEFAULT_DNS_SERVER);
             MOVED_TO_GLOBAL.add(Settings.Global.PREFERRED_NETWORK_MODE);
             MOVED_TO_GLOBAL.add(Settings.Global.WEBVIEW_DATA_REDUCTION_PROXY_KEY);
+            MOVED_TO_GLOBAL.add(Settings.Global.SECURE_FRP_MODE);
         }
 
         /** @hide */
@@ -7072,7 +7073,10 @@
          * device is removed from this mode.
          * <p>
          * Type: int (0 for false, 1 for true)
+         *
+         * @deprecated Use Global.SECURE_FRP_MODE
          */
+        @Deprecated
         @Readable
         public static final String SECURE_FRP_MODE = "secure_frp_mode";
 
@@ -10719,6 +10723,49 @@
                 "back_gesture_inset_scale_right";
 
         /**
+         * Indicates whether the trackpad back gesture is enabled.
+         * <p>Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        public static final String TRACKPAD_GESTURE_BACK_ENABLED = "trackpad_gesture_back_enabled";
+
+        /**
+         * Indicates whether the trackpad home gesture is enabled.
+         * <p>Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        public static final String TRACKPAD_GESTURE_HOME_ENABLED = "trackpad_gesture_home_enabled";
+
+        /**
+         * Indicates whether the trackpad overview gesture is enabled.
+         * <p>Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        public static final String TRACKPAD_GESTURE_OVERVIEW_ENABLED =
+                "trackpad_gesture_overview_enabled";
+
+        /**
+         * Indicates whether the trackpad notification gesture is enabled.
+         * <p>Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        public static final String TRACKPAD_GESTURE_NOTIFICATION_ENABLED =
+                "trackpad_gesture_notification_enabled";
+
+        /**
+         * Indicates whether the trackpad quick switch gesture is enabled.
+         * <p>Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        public static final String TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED =
+                "trackpad_gesture_quick_switch_enabled";
+
+        /**
          * Current provider of proximity-based sharing services.
          * Default value in @string/config_defaultNearbySharingComponent.
          * No VALIDATOR as this setting will not be backed up.
@@ -11897,7 +11944,21 @@
         public static final String DEVICE_PROVISIONED = "device_provisioned";
 
         /**
-         * Whether bypassing the device policy management role holder qualifcation is allowed,
+         * Indicates whether the device is under restricted secure FRP mode.
+         * Secure FRP mode is enabled when the device is under FRP. On solving of FRP challenge,
+         * device is removed from this mode.
+         * <p>
+         * Type: int (0 for false, 1 for true)
+         *
+         * @hide
+         */
+        @SystemApi
+        @Readable
+        @SuppressLint("NoSettingsProvider")
+        public static final String SECURE_FRP_MODE = "secure_frp_mode";
+
+        /**
+         * Whether bypassing the device policy management role holder qualification is allowed,
          * (0 = false, 1 = true).
          *
          * @hide
diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractHotwordDetector.java
index 28357ef..c90ab67 100644
--- a/core/java/android/service/voice/AbstractHotwordDetector.java
+++ b/core/java/android/service/voice/AbstractHotwordDetector.java
@@ -25,7 +25,9 @@
 import android.app.compat.CompatChanges;
 import android.media.AudioFormat;
 import android.media.permission.Identity;
+import android.os.Binder;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
@@ -49,19 +51,20 @@
     private final IVoiceInteractionManagerService mManagerService;
     private final Handler mHandler;
     private final HotwordDetector.Callback mCallback;
-    private final int mDetectorType;
     private Consumer<AbstractHotwordDetector> mOnDestroyListener;
     private final AtomicBoolean mIsDetectorActive;
+    /**
+     * A token which is used by voice interaction system service to identify different detectors.
+     */
+    private final IBinder mToken = new Binder();
 
     AbstractHotwordDetector(
             IVoiceInteractionManagerService managerService,
-            HotwordDetector.Callback callback,
-            int detectorType) {
+            HotwordDetector.Callback callback) {
         mManagerService = managerService;
         // TODO: this needs to be supplied from above
         mHandler = new Handler(Looper.getMainLooper());
         mCallback = callback;
-        mDetectorType = detectorType;
         mIsDetectorActive = new AtomicBoolean(true);
     }
 
@@ -94,6 +97,7 @@
                     audioStream,
                     audioFormat,
                     options,
+                    mToken,
                     new BinderCallback(mHandler, mCallback));
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
@@ -111,7 +115,7 @@
         }
         throwIfDetectorIsNoLongerActive();
         try {
-            mManagerService.updateState(options, sharedMemory);
+            mManagerService.updateState(options, sharedMemory, mToken);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -128,7 +132,7 @@
         Identity identity = new Identity();
         identity.packageName = ActivityThread.currentOpPackageName();
         try {
-            mManagerService.initAndVerifyDetector(identity, options, sharedMemory, callback,
+            mManagerService.initAndVerifyDetector(identity, options, sharedMemory, mToken, callback,
                     detectorType);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -151,6 +155,11 @@
             return;
         }
         mIsDetectorActive.set(false);
+        try {
+            mManagerService.destroyDetector(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
         synchronized (mLock) {
             mOnDestroyListener.accept(this);
         }
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 320280b..9008bf7 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -293,7 +293,7 @@
     private final Handler mHandler;
     private final IBinder mBinder = new Binder();
     private final int mTargetSdkVersion;
-    private final boolean mSupportHotwordDetectionService;
+    private final boolean mSupportSandboxedDetectionService;
 
     @GuardedBy("mLock")
     private boolean mIsAvailabilityOverriddenByTestApi = false;
@@ -756,7 +756,7 @@
      * @param callback A non-null Callback for receiving the recognition events.
      * @param modelManagementService A service that allows management of sound models.
      * @param targetSdkVersion The target SDK version.
-     * @param supportHotwordDetectionService {@code true} if HotwordDetectionService should be
+     * @param SupportSandboxedDetectionService {@code true} if HotwordDetectionService should be
      * triggered, otherwise {@code false}.
      *
      * @hide
@@ -764,10 +764,8 @@
     public AlwaysOnHotwordDetector(String text, Locale locale, Callback callback,
             KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
             IVoiceInteractionManagerService modelManagementService, int targetSdkVersion,
-            boolean supportHotwordDetectionService) {
-        super(modelManagementService, callback,
-                supportHotwordDetectionService ? DETECTOR_TYPE_TRUSTED_HOTWORD_DSP
-                        : DETECTOR_TYPE_NORMAL);
+            boolean supportSandboxedDetectionService) {
+        super(modelManagementService, callback);
 
         mHandler = new MyHandler();
         mText = text;
@@ -777,12 +775,12 @@
         mInternalCallback = new SoundTriggerListener(mHandler);
         mModelManagementService = modelManagementService;
         mTargetSdkVersion = targetSdkVersion;
-        mSupportHotwordDetectionService = supportHotwordDetectionService;
+        mSupportSandboxedDetectionService = supportSandboxedDetectionService;
     }
 
     @Override
     void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) {
-        if (mSupportHotwordDetectionService) {
+        if (mSupportSandboxedDetectionService) {
             initAndVerifyDetector(options, sharedMemory, mInternalCallback,
                     DETECTOR_TYPE_TRUSTED_HOTWORD_DSP);
         }
@@ -814,7 +812,7 @@
     public final void updateState(@Nullable PersistableBundle options,
             @Nullable SharedMemory sharedMemory) throws IllegalDetectorStateException {
         synchronized (mLock) {
-            if (!mSupportHotwordDetectionService) {
+            if (!mSupportSandboxedDetectionService) {
                 throw new IllegalStateException(
                         "updateState called, but it doesn't support hotword detection service");
             }
@@ -1410,8 +1408,8 @@
      * @hide
      */
     @Override
-    public boolean isUsingHotwordDetectionService() {
-        return mSupportHotwordDetectionService;
+    public boolean isUsingSandboxedDetectionService() {
+        return mSupportSandboxedDetectionService;
     }
 
     /**
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index 552a793..a47c096 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -140,7 +140,7 @@
     @Nullable
     private IRecognitionServiceManager mIRecognitionServiceManager;
 
-    private final IHotwordDetectionService mInterface = new IHotwordDetectionService.Stub() {
+    private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.Stub() {
         @Override
         public void detectFromDspSource(
                 SoundTrigger.KeyphraseRecognitionEvent event,
diff --git a/core/java/android/service/voice/HotwordDetector.java b/core/java/android/service/voice/HotwordDetector.java
index 7112dc6..b7f7d54 100644
--- a/core/java/android/service/voice/HotwordDetector.java
+++ b/core/java/android/service/voice/HotwordDetector.java
@@ -178,7 +178,7 @@
     /**
      * @hide
      */
-    default boolean isUsingHotwordDetectionService() {
+    default boolean isUsingSandboxedDetectionService() {
         throw new UnsupportedOperationException("Not implemented. Must override in a subclass.");
     }
 
diff --git a/core/java/android/service/voice/IHotwordDetectionService.aidl b/core/java/android/service/voice/ISandboxedDetectionService.aidl
similarity index 94%
rename from core/java/android/service/voice/IHotwordDetectionService.aidl
rename to core/java/android/service/voice/ISandboxedDetectionService.aidl
index 9ef9307..5537fd1 100644
--- a/core/java/android/service/voice/IHotwordDetectionService.aidl
+++ b/core/java/android/service/voice/ISandboxedDetectionService.aidl
@@ -29,11 +29,11 @@
 import android.speech.IRecognitionServiceManager;
 
 /**
- * Provide the interface to communicate with hotword detection service.
+ * Provide the interface to communicate with sandboxed detection service.
  *
  * @hide
  */
-oneway interface IHotwordDetectionService {
+oneway interface ISandboxedDetectionService {
     void detectFromDspSource(
         in SoundTrigger.KeyphraseRecognitionEvent event,
         in AudioFormat audioFormat,
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index 11688df..f1b7745 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -59,7 +59,7 @@
             IVoiceInteractionManagerService managerService,
             AudioFormat audioFormat,
             HotwordDetector.Callback callback) {
-        super(managerService, callback, DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE);
+        super(managerService, callback);
 
         mManagerService = managerService;
         mAudioFormat = audioFormat;
@@ -129,7 +129,7 @@
      * @hide
      */
     @Override
-    public boolean isUsingHotwordDetectionService() {
+    public boolean isUsingSandboxedDetectionService() {
         return true;
     }
 
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index 7c125c7..a59578e 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -430,7 +430,7 @@
                 safelyShutdownAllHotwordDetectors();
             } else {
                 for (HotwordDetector detector : mActiveHotwordDetectors) {
-                    if (detector.isUsingHotwordDetectionService()
+                    if (detector.isUsingSandboxedDetectionService()
                             != supportHotwordDetectionService) {
                         throw new IllegalStateException(
                                 "It disallows to create trusted and non-trusted detectors "
@@ -513,7 +513,7 @@
                 safelyShutdownAllHotwordDetectors();
             } else {
                 for (HotwordDetector detector : mActiveHotwordDetectors) {
-                    if (!detector.isUsingHotwordDetectionService()) {
+                    if (!detector.isUsingSandboxedDetectionService()) {
                         throw new IllegalStateException(
                                 "It disallows to create trusted and non-trusted detectors "
                                         + "at the same time.");
@@ -605,7 +605,7 @@
 
     private void shutdownHotwordDetectionServiceIfRequiredLocked() {
         for (HotwordDetector detector : mActiveHotwordDetectors) {
-            if (detector.isUsingHotwordDetectionService()) {
+            if (detector.isUsingSandboxedDetectionService()) {
                 return;
             }
         }
diff --git a/core/java/android/service/voice/VoiceInteractionServiceInfo.java b/core/java/android/service/voice/VoiceInteractionServiceInfo.java
index ff03cc1..af29961 100644
--- a/core/java/android/service/voice/VoiceInteractionServiceInfo.java
+++ b/core/java/android/service/voice/VoiceInteractionServiceInfo.java
@@ -46,6 +46,7 @@
     private String mSessionService;
     private String mRecognitionService;
     private String mHotwordDetectionService;
+    private String mVisualQueryDetectionService;
     private String mSettingsActivity;
     private boolean mSupportsAssist;
     private boolean mSupportsLaunchFromKeyguard;
@@ -137,6 +138,8 @@
                     R.styleable.VoiceInteractionService_supportsLocalInteraction, false);
             mHotwordDetectionService = array.getString(com.android.internal.R.styleable
                     .VoiceInteractionService_hotwordDetectionService);
+            mVisualQueryDetectionService = array.getString(com.android.internal.R.styleable
+                    .VoiceInteractionService_visualQueryDetectionService);
             array.recycle();
             if (mSessionService == null) {
                 mParseError = "No sessionService specified";
@@ -190,4 +193,9 @@
     public String getHotwordDetectionService() {
         return mHotwordDetectionService;
     }
+
+    @Nullable
+    public String getVisualQueryDetectionService() {
+        return mVisualQueryDetectionService;
+    }
 }
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 0a1538de..519647d 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -45,6 +45,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.listeners.ListenerExecutor;
+import com.android.internal.telephony.ICarrierConfigChangeListener;
 import com.android.internal.telephony.ICarrierPrivilegesCallback;
 import com.android.internal.telephony.IOnSubscriptionsChangedListener;
 import com.android.internal.telephony.ITelephonyRegistry;
@@ -54,8 +55,10 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
@@ -89,6 +92,14 @@
             IOnSubscriptionsChangedListener> mOpportunisticSubscriptionChangedListenerMap
             = new HashMap<>();
 
+    /**
+     * A mapping between {@link CarrierConfigManager.CarrierConfigChangeListener} and its callback
+     * ICarrierConfigChangeListener.
+     */
+    private final ConcurrentHashMap<CarrierConfigManager.CarrierConfigChangeListener,
+                ICarrierConfigChangeListener>
+            mCarrierConfigChangeListenerMap = new ConcurrentHashMap<>();
+
 
     /** @hide **/
     public TelephonyRegistryManager(@NonNull Context context) {
@@ -1412,4 +1423,94 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Register a {@link android.telephony.CarrierConfigManager.CarrierConfigChangeListener} to get
+     * notification when carrier configurations have changed.
+     *
+     * @param executor The executor on which the callback will be executed.
+     * @param listener The CarrierConfigChangeListener to be registered with.
+     */
+    public void addCarrierConfigChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull CarrierConfigManager.CarrierConfigChangeListener listener) {
+        Objects.requireNonNull(executor, "Executor should be non-null.");
+        Objects.requireNonNull(listener, "Listener should be non-null.");
+        if (mCarrierConfigChangeListenerMap.get(listener) != null) {
+            Log.e(TAG, "registerCarrierConfigChangeListener: listener already present");
+            return;
+        }
+
+        ICarrierConfigChangeListener callback = new ICarrierConfigChangeListener.Stub() {
+            @Override
+            public void onCarrierConfigChanged(int slotIndex, int subId, int carrierId,
+                    int specificCarrierId) {
+                Log.d(TAG, "onCarrierConfigChanged call in ICarrierConfigChangeListener callback");
+                final long identify = Binder.clearCallingIdentity();
+                try {
+                    executor.execute(() -> listener.onCarrierConfigChanged(slotIndex, subId,
+                            carrierId, specificCarrierId));
+                } finally {
+                    Binder.restoreCallingIdentity(identify);
+                }
+            }
+        };
+
+        try {
+            sRegistry.addCarrierConfigChangeListener(callback,
+                    mContext.getOpPackageName(), mContext.getAttributionTag());
+            mCarrierConfigChangeListenerMap.put(listener, callback);
+        } catch (RemoteException re) {
+            // system server crashes
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregister to stop the notification when carrier configurations changed.
+     *
+     * @param listener The CarrierConfigChangeListener to be unregistered with.
+     */
+    public void removeCarrierConfigChangedListener(
+            @NonNull CarrierConfigManager.CarrierConfigChangeListener listener) {
+        Objects.requireNonNull(listener, "Listener should be non-null.");
+        if (mCarrierConfigChangeListenerMap.get(listener) == null) {
+            Log.e(TAG, "removeCarrierConfigChangedListener: listener was not present");
+            return;
+        }
+
+        try {
+            sRegistry.removeCarrierConfigChangeListener(
+                    mCarrierConfigChangeListenerMap.get(listener), mContext.getOpPackageName());
+            mCarrierConfigChangeListenerMap.remove(listener);
+        } catch (RemoteException re) {
+            // System sever crashes
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Notify the registrants the carrier configurations have changed.
+     *
+     * @param slotIndex         The SIM slot index on which to monitor and get notification.
+     * @param subId             The subscription on the SIM slot. May be
+     *                          {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}.
+     * @param carrierId         The optional carrier Id, may be
+     *                          {@link TelephonyManager#UNKNOWN_CARRIER_ID}.
+     * @param specificCarrierId The optional specific carrier Id, may be {@link
+     *                          TelephonyManager#UNKNOWN_CARRIER_ID}.
+     */
+    public void notifyCarrierConfigChanged(int slotIndex, int subId, int carrierId,
+            int specificCarrierId) {
+        // Only validate slotIndex, all others are optional and allowed to be invalid
+        if (!SubscriptionManager.isValidPhoneId(slotIndex)) {
+            Log.e(TAG, "notifyCarrierConfigChanged, ignored: invalid slotIndex " + slotIndex);
+            return;
+        }
+        try {
+            sRegistry.notifyCarrierConfigChanged(slotIndex, subId, carrierId, specificCarrierId);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index 2c4f38d..897e23a 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -196,6 +196,7 @@
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD);
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE);
+        PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA);
     }
 
     /**
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java
index c692981..3b082bc 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -140,13 +140,13 @@
     }
 
     /**
-     * Returns a SyncTarget that can be used to sync {@link AttachedSurfaceControl} in a
+     * Returns a SurfaceSyncGroup that can be used to sync {@link AttachedSurfaceControl} in a
      * {@link SurfaceSyncGroup}
      *
      * @hide
      */
     @Nullable
-    default SurfaceSyncGroup.SyncTarget getSyncTarget() {
+    default SurfaceSyncGroup getOrCreateSurfaceSyncGroup() {
         return null;
     }
 }
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index 06c1b25..61582cd 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -892,13 +892,18 @@
      * The use of this button does not usually correspond to the function of an eraser.
      */
     public static final int KEYCODE_STYLUS_BUTTON_TAIL = 311;
+    /**
+     * Key code constant: To open recent apps view (a.k.a. Overview).
+     * This key is handled by the framework and is never delivered to applications.
+     */
+    public static final int KEYCODE_RECENT_APPS = 312;
 
    /**
      * Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent.
      * @hide
      */
     @TestApi
-    public static final int LAST_KEYCODE = KEYCODE_STYLUS_BUTTON_TAIL;
+    public static final int LAST_KEYCODE = KEYCODE_RECENT_APPS;
 
     // NOTE: If you add a new keycode here you must also add it to:
     //  isSystem()
@@ -2021,6 +2026,7 @@
             case KeyEvent.KEYCODE_MENU:
             case KeyEvent.KEYCODE_SOFT_RIGHT:
             case KeyEvent.KEYCODE_HOME:
+            case KeyEvent.KEYCODE_RECENT_APPS:
             case KeyEvent.KEYCODE_BACK:
             case KeyEvent.KEYCODE_CALL:
             case KeyEvent.KEYCODE_ENDCALL:
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index c8a5d8d..4fbb249 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -2199,6 +2199,7 @@
     }
 
     /** @hide */
+    @TestApi
     @Override
     public int getDisplayId() {
         return nativeGetDisplayId(mNativePtr);
diff --git a/core/java/android/view/ScrollCaptureConnection.java b/core/java/android/view/ScrollCaptureConnection.java
index c50f70a..f0c7909 100644
--- a/core/java/android/view/ScrollCaptureConnection.java
+++ b/core/java/android/view/ScrollCaptureConnection.java
@@ -129,7 +129,7 @@
             close();
         }
         mCancellation = null;
-        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, START_CAPTURE, mTraceId);
+        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
     }
 
     @BinderThread
@@ -164,7 +164,7 @@
         } finally {
             mCancellation = null;
         }
-        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, REQUEST_IMAGE, mTraceId);
+        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
     }
 
     @BinderThread
@@ -200,8 +200,8 @@
             mCancellation = null;
             close();
         }
-        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, END_CAPTURE, mTraceId);
-        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, SESSION, mTraceId);
+        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
+        Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
     }
 
     @Override
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 1889772..0a134be 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -404,14 +404,6 @@
     }
 
     /**
-     * @hide
-     */
-    @TestApi
-    public void relayout(WindowManager.LayoutParams attrs) {
-        relayout(attrs, SurfaceControl.Transaction::apply);
-    }
-
-    /**
      * Forces relayout and draw and allows to set a custom callback when it is finished
      * @hide
      */
@@ -423,6 +415,14 @@
     }
 
     /**
+     * @hide
+     */
+    @TestApi
+    public void relayout(WindowManager.LayoutParams attrs) {
+        mViewRoot.setLayoutParams(attrs, false);
+    }
+
+    /**
      * Modify the size of the root view.
      *
      * @param width Width in pixels
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 33ea92d..9db084e 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -982,8 +982,8 @@
 
                 final boolean redrawNeeded = sizeChanged || creating || hintChanged
                         || (mVisible && !mDrawFinished) || alphaChanged || relativeZChanged;
-                boolean shouldSyncBuffer =
-                        redrawNeeded && viewRoot.wasRelayoutRequested() && viewRoot.isInLocalSync();
+                boolean shouldSyncBuffer = redrawNeeded && viewRoot.wasRelayoutRequested()
+                        && viewRoot.isInWMSRequestedSync();
                 SyncBufferTransactionCallback syncBufferTransactionCallback = null;
                 if (shouldSyncBuffer) {
                     syncBufferTransactionCallback = new SyncBufferTransactionCallback();
@@ -1073,35 +1073,34 @@
     private void handleSyncBufferCallback(SurfaceHolder.Callback[] callbacks,
             SyncBufferTransactionCallback syncBufferTransactionCallback) {
 
-        getViewRootImpl().addToSync((parentSyncGroup, syncBufferCallback) ->
-                redrawNeededAsync(callbacks, () -> {
-                    Transaction t = null;
-                    if (mBlastBufferQueue != null) {
-                        mBlastBufferQueue.stopContinuousSyncTransaction();
-                        t = syncBufferTransactionCallback.waitForTransaction();
-                    }
+        final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup();
+        getViewRootImpl().addToSync(surfaceSyncGroup);
+        redrawNeededAsync(callbacks, () -> {
+            Transaction t = null;
+            if (mBlastBufferQueue != null) {
+                mBlastBufferQueue.stopContinuousSyncTransaction();
+                t = syncBufferTransactionCallback.waitForTransaction();
+            }
 
-                    syncBufferCallback.onTransactionReady(t);
-                    onDrawFinished();
-                }));
+            surfaceSyncGroup.onTransactionReady(t);
+            onDrawFinished();
+        });
     }
 
     private void handleSyncNoBuffer(SurfaceHolder.Callback[] callbacks) {
-        final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+        final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup();
         synchronized (mSyncGroups) {
-            mSyncGroups.add(syncGroup);
+            mSyncGroups.add(surfaceSyncGroup);
         }
 
-        syncGroup.addToSync((parentSyncGroup, syncBufferCallback) ->
-                redrawNeededAsync(callbacks, () -> {
-                    syncBufferCallback.onTransactionReady(null);
-                    onDrawFinished();
-                    synchronized (mSyncGroups) {
-                        mSyncGroups.remove(syncGroup);
-                    }
-                }));
+        redrawNeededAsync(callbacks, () -> {
+            synchronized (mSyncGroups) {
+                mSyncGroups.remove(surfaceSyncGroup);
+            }
+            surfaceSyncGroup.onTransactionReady(null);
+            onDrawFinished();
+        });
 
-        syncGroup.markSyncReady();
     }
 
     private void redrawNeededAsync(SurfaceHolder.Callback[] callbacks,
@@ -1119,7 +1118,7 @@
         if (viewRoot != null) {
             synchronized (mSyncGroups) {
                 for (SurfaceSyncGroup syncGroup : mSyncGroups) {
-                    viewRoot.mergeSync(syncGroup);
+                    viewRoot.addToSync(syncGroup);
                 }
             }
         }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 33b58d7..b2973ef 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -595,19 +595,21 @@
     String mLastPerformDrawSkippedReason;
     /** The reason the last call to performTraversals() returned without drawing */
     String mLastPerformTraversalsSkipDrawReason;
-    /** The state of the local sync, if one is in progress. Can be one of the states below. */
-    int mLocalSyncState;
+    /** The state of the WMS requested sync, if one is in progress. Can be one of the states
+     * below. */
+    int mWmsRequestSyncGroupState;
 
-    // The possible states of the local sync, see createSyncIfNeeded()
-    private final int LOCAL_SYNC_NONE = 0;
-    private final int LOCAL_SYNC_PENDING = 1;
-    private final int LOCAL_SYNC_RETURNED = 2;
-    private final int LOCAL_SYNC_MERGED = 3;
+    // The possible states of the WMS requested sync, see createSyncIfNeeded()
+    private static final int WMS_SYNC_NONE = 0;
+    private static final int WMS_SYNC_PENDING = 1;
+    private static final int WMS_SYNC_RETURNED = 2;
+    private static final int WMS_SYNC_MERGED = 3;
 
     /**
-     * Set whether the draw should send the buffer to system server. When set to true, VRI will
-     * create a sync transaction with BBQ and send the resulting buffer to system server. If false,
-     * VRI will not try to sync a buffer in BBQ, but still report when a draw occurred.
+     * Set whether the requested SurfaceSyncGroup should sync the buffer. When set to true, VRI will
+     * create a sync transaction with BBQ and send the resulting buffer back to the
+     * SurfaceSyncGroup. If false, VRI will not try to sync a buffer in BBQ, but still report when a
+     * draw occurred.
      */
     private boolean mSyncBuffer = false;
 
@@ -849,8 +851,19 @@
         return mHandwritingInitiator;
     }
 
-    private SurfaceSyncGroup mSyncGroup;
-    private SurfaceSyncGroup.TransactionReadyCallback mTransactionReadyCallback;
+    /**
+     * A SurfaceSyncGroup that is created when WMS requested to sync the buffer
+     */
+    private SurfaceSyncGroup mWmsRequestSyncGroup;
+
+    /**
+     * The SurfaceSyncGroup that represents the active VRI SurfaceSyncGroup. This is non null if
+     * anyone requested the SurfaceSyncGroup for this VRI to ensure that anyone trying to sync with
+     * this VRI are collected together. The SurfaceSyncGroup is cleared when the VRI draws since
+     * that is the stop point where all changes are have been applied. A new SurfaceSyncGroup is
+     * created after that point when something wants to sync VRI again.
+     */
+    private SurfaceSyncGroup mActiveSurfaceSyncGroup;
 
     private static final Object sSyncProgressLock = new Object();
     // The count needs to be static since it's used to enable or disable RT animations which is
@@ -3643,6 +3656,12 @@
         boolean cancelAndRedraw = cancelDueToPreDrawListener
                  || (cancelDraw && mDrewOnceForSync);
         if (!cancelAndRedraw) {
+            // A sync was already requested before the WMS requested sync. This means we need to
+            // sync the buffer, regardless if WMS wants to sync the buffer.
+            if (mActiveSurfaceSyncGroup != null) {
+                mSyncBuffer = true;
+            }
+
             createSyncIfNeeded();
             mDrewOnceForSync = true;
         }
@@ -3656,8 +3675,8 @@
                 mPendingTransitions.clear();
             }
 
-            if (mTransactionReadyCallback != null) {
-                mTransactionReadyCallback.onTransactionReady(null);
+            if (mActiveSurfaceSyncGroup != null) {
+                mActiveSurfaceSyncGroup.onTransactionReady(null);
             }
         } else if (cancelAndRedraw) {
             mLastPerformTraversalsSkipDrawReason = cancelDueToPreDrawListener
@@ -3672,8 +3691,8 @@
                 }
                 mPendingTransitions.clear();
             }
-            if (!performDraw() && mTransactionReadyCallback != null) {
-                mTransactionReadyCallback.onTransactionReady(null);
+            if (!performDraw() && mActiveSurfaceSyncGroup != null) {
+                mActiveSurfaceSyncGroup.onTransactionReady(null);
             }
         }
 
@@ -3687,39 +3706,40 @@
         if (!cancelAndRedraw) {
             mReportNextDraw = false;
             mLastReportNextDrawReason = null;
-            mTransactionReadyCallback = null;
+            mActiveSurfaceSyncGroup = null;
             mSyncBuffer = false;
-            if (isInLocalSync()) {
-                mSyncGroup.markSyncReady();
-                mSyncGroup = null;
-                mLocalSyncState = LOCAL_SYNC_NONE;
+            if (isInWMSRequestedSync()) {
+                mWmsRequestSyncGroup.onTransactionReady(null);
+                mWmsRequestSyncGroup = null;
+                mWmsRequestSyncGroupState = WMS_SYNC_NONE;
             }
         }
     }
 
     private void createSyncIfNeeded() {
-        // Started a sync already or there's nothing needing to sync
-        if (isInLocalSync() || !mReportNextDraw) {
+        // WMS requested sync already started or there's nothing needing to sync
+        if (isInWMSRequestedSync() || !mReportNextDraw) {
             return;
         }
 
         final int seqId = mSyncSeqId;
-        mLocalSyncState = LOCAL_SYNC_PENDING;
-        mSyncGroup = new SurfaceSyncGroup(transaction -> {
-            mLocalSyncState = LOCAL_SYNC_RETURNED;
+        mWmsRequestSyncGroupState = WMS_SYNC_PENDING;
+        mWmsRequestSyncGroup = new SurfaceSyncGroup(t -> {
+            mWmsRequestSyncGroupState = WMS_SYNC_RETURNED;
             // Callback will be invoked on executor thread so post to main thread.
             mHandler.postAtFrontOfQueue(() -> {
-                if (transaction != null) {
-                    mSurfaceChangedTransaction.merge(transaction);
+                if (t != null) {
+                    mSurfaceChangedTransaction.merge(t);
                 }
-                mLocalSyncState = LOCAL_SYNC_MERGED;
+                mWmsRequestSyncGroupState = WMS_SYNC_MERGED;
                 reportDrawFinished(seqId);
             });
         });
         if (DEBUG_BLAST) {
-            Log.d(mTag, "Setup new sync id=" + mSyncGroup);
+            Log.d(mTag, "Setup new sync id=" + mWmsRequestSyncGroup);
         }
-        mSyncGroup.addToSync(mSyncTarget);
+
+        mWmsRequestSyncGroup.addToSync(this);
         notifySurfaceSyncStarted();
     }
 
@@ -4370,19 +4390,11 @@
         return mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled();
     }
 
-    void addToSync(SurfaceSyncGroup.SyncTarget syncable) {
-        if (!isInLocalSync()) {
-            return;
-        }
-        mSyncGroup.addToSync(syncable);
-    }
-
     /**
-     * This VRI is currently in the middle of a sync request, but specifically one initiated from
-     * within VRI.
+     * This VRI is currently in the middle of a sync request that was initiated by WMS.
      */
-    public boolean isInLocalSync() {
-        return mSyncGroup != null;
+    public boolean isInWMSRequestedSync() {
+        return mWmsRequestSyncGroup != null;
     }
 
     private void addFrameCommitCallbackIfNeeded() {
@@ -4449,7 +4461,7 @@
             return false;
         }
 
-        final boolean fullRedrawNeeded = mFullRedrawNeeded || mTransactionReadyCallback != null;
+        final boolean fullRedrawNeeded = mFullRedrawNeeded || mActiveSurfaceSyncGroup != null;
         mFullRedrawNeeded = false;
 
         mIsDrawing = true;
@@ -4457,9 +4469,9 @@
 
         addFrameCommitCallbackIfNeeded();
 
-        boolean usingAsyncReport = isHardwareEnabled() && mTransactionReadyCallback != null;
+        boolean usingAsyncReport = isHardwareEnabled() && mActiveSurfaceSyncGroup != null;
         if (usingAsyncReport) {
-            registerCallbacksForSync(mSyncBuffer, mTransactionReadyCallback);
+            registerCallbacksForSync(mSyncBuffer, mActiveSurfaceSyncGroup);
         } else if (mHasPendingTransactions) {
             // These callbacks are only needed if there's no sync involved and there were calls to
             // applyTransactionOnDraw. These callbacks check if the draw failed for any reason and
@@ -4510,11 +4522,10 @@
             }
 
             if (mSurfaceHolder != null && mSurface.isValid()) {
-                final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback =
-                        mTransactionReadyCallback;
+                final SurfaceSyncGroup surfaceSyncGroup = mActiveSurfaceSyncGroup;
                 SurfaceCallbackHelper sch = new SurfaceCallbackHelper(() ->
-                        mHandler.post(() -> transactionReadyCallback.onTransactionReady(null)));
-                mTransactionReadyCallback = null;
+                        mHandler.post(() -> surfaceSyncGroup.onTransactionReady(null)));
+                mActiveSurfaceSyncGroup = null;
 
                 SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
 
@@ -4525,8 +4536,8 @@
                 }
             }
         }
-        if (mTransactionReadyCallback != null && !usingAsyncReport) {
-            mTransactionReadyCallback.onTransactionReady(null);
+        if (mActiveSurfaceSyncGroup != null && !usingAsyncReport) {
+            mActiveSurfaceSyncGroup.onTransactionReady(null);
         }
         if (mPerformContentCapture) {
             performContentCaptureInitialReport();
@@ -8616,8 +8627,8 @@
             writer.println(innerPrefix + "mLastPerformDrawFailedReason="
                 + mLastPerformDrawSkippedReason);
         }
-        if (mLocalSyncState != LOCAL_SYNC_NONE) {
-            writer.println(innerPrefix + "mLocalSyncState=" + mLocalSyncState);
+        if (mWmsRequestSyncGroupState != WMS_SYNC_NONE) {
+            writer.println(innerPrefix + "mWmsRequestSyncGroupState=" + mWmsRequestSyncGroupState);
         }
         writer.println(innerPrefix + "mLastReportedMergedConfiguration="
                 + mLastReportedMergedConfiguration);
@@ -11205,7 +11216,7 @@
     }
 
     private void registerCallbacksForSync(boolean syncBuffer,
-            final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) {
+            final SurfaceSyncGroup surfaceSyncGroup) {
         if (!isHardwareEnabled()) {
             return;
         }
@@ -11232,7 +11243,7 @@
                 // pendingDrawFinished.
                 if ((syncResult
                         & (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) {
-                    transactionReadyCallback.onTransactionReady(
+                    surfaceSyncGroup.onTransactionReady(
                             mBlastBufferQueue.gatherPendingTransactions(frame));
                     return null;
                 }
@@ -11243,7 +11254,7 @@
 
                 if (syncBuffer) {
                     mBlastBufferQueue.syncNextTransaction(
-                            transactionReadyCallback::onTransactionReady);
+                            surfaceSyncGroup::onTransactionReady);
                 }
 
                 return didProduceBuffer -> {
@@ -11263,7 +11274,7 @@
                         // since the frame didn't draw on this vsync. It's possible the frame will
                         // draw later, but it's better to not be sync than to block on a frame that
                         // may never come.
-                        transactionReadyCallback.onTransactionReady(
+                        surfaceSyncGroup.onTransactionReady(
                                 mBlastBufferQueue.gatherPendingTransactions(frame));
                         return;
                     }
@@ -11272,35 +11283,23 @@
                     // syncNextTransaction callback. Instead, just report back to the Syncer so it
                     // knows that this sync request is complete.
                     if (!syncBuffer) {
-                        transactionReadyCallback.onTransactionReady(null);
+                        surfaceSyncGroup.onTransactionReady(null);
                     }
                 };
             }
         });
     }
 
-    public final SurfaceSyncGroup.SyncTarget mSyncTarget = new SurfaceSyncGroup.SyncTarget() {
-        @Override
-        public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
-                SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) {
-            updateSyncInProgressCount(parentSyncGroup);
-            if (!isInLocalSync()) {
-                // Always sync the buffer if the sync request did not come from VRI.
-                mSyncBuffer = true;
-            }
-
-            if (mTransactionReadyCallback != null) {
-                Log.d(mTag, "Already set sync for the next draw.");
-                mTransactionReadyCallback.onTransactionReady(null);
-            }
-            if (DEBUG_BLAST) {
-                Log.d(mTag, "Setting syncFrameCallback");
-            }
-            mTransactionReadyCallback = transactionReadyCallback;
+    @Override
+    public SurfaceSyncGroup getOrCreateSurfaceSyncGroup() {
+        if (mActiveSurfaceSyncGroup == null) {
+            mActiveSurfaceSyncGroup = new SurfaceSyncGroup();
+            updateSyncInProgressCount(mActiveSurfaceSyncGroup);
             if (!mIsInTraversal && !mTraversalScheduled) {
                 scheduleTraversals();
             }
         }
+        return mActiveSurfaceSyncGroup;
     };
 
     private final Executor mSimpleExecutor = Runnable::run;
@@ -11325,15 +11324,10 @@
         });
     }
 
-    @Override
-    public SurfaceSyncGroup.SyncTarget getSyncTarget() {
-        return mSyncTarget;
-    }
-
-    void mergeSync(SurfaceSyncGroup otherSyncGroup) {
-        if (!isInLocalSync()) {
+    void addToSync(SurfaceSyncGroup syncable) {
+        if (mActiveSurfaceSyncGroup == null) {
             return;
         }
-        mSyncGroup.merge(otherSyncGroup);
+        mActiveSurfaceSyncGroup.addToSync(syncable, false /* parentSyncGroupMerge */);
     }
 }
diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
index 44b6deb..a757236 100644
--- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
+++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
@@ -242,11 +242,7 @@
             super(context, executor, new AccessibilityService.Callbacks() {
                 @Override
                 public void onAccessibilityEvent(AccessibilityEvent event) {
-                    // TODO(254545943): Remove check when event processing is done more upstream in
-                    // AccessibilityManagerService.
-                    if (event.getDisplayId() == mDisplayId) {
-                        AccessibilityDisplayProxy.this.onAccessibilityEvent(event);
-                    }
+                    AccessibilityDisplayProxy.this.onAccessibilityEvent(event);
                 }
 
                 @Override
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 423c560..9abbba9 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -25,6 +25,7 @@
 import android.accessibilityservice.AccessibilityShortcutInfo;
 import android.annotation.CallbackExecutor;
 import android.annotation.ColorInt;
+import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -75,6 +76,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 /**
@@ -138,6 +140,21 @@
     public static final int AUTOCLICK_DELAY_DEFAULT = 600;
 
     /**
+     * The contrast is defined as a float in [-1, 1], with a default value of 0.
+     * @hide
+     */
+    public static final float CONTRAST_MIN_VALUE = -1f;
+
+    /** @hide */
+    public static final float CONTRAST_MAX_VALUE = 1f;
+
+    /** @hide */
+    public static final float CONTRAST_DEFAULT_VALUE = 0f;
+
+    /** @hide */
+    public static final float CONTRAST_NOT_SET = Float.MIN_VALUE;
+
+    /**
      * Activity action: Launch UI to manage which accessibility service or feature is assigned
      * to the navigation bar Accessibility button.
      * <p>
@@ -246,6 +263,8 @@
     @UnsupportedAppUsage(trackingBug = 123768939L)
     boolean mIsHighTextContrastEnabled;
 
+    private float mUiContrast;
+
     boolean mIsAudioDescriptionByDefaultRequested;
 
     // accessibility tracing state
@@ -270,6 +289,9 @@
     private final ArrayMap<HighTextContrastChangeListener, Handler>
             mHighTextContrastStateChangeListeners = new ArrayMap<>();
 
+    private final ArrayMap<UiContrastChangeListener, Executor>
+            mUiContrastChangeListeners = new ArrayMap<>();
+
     private final ArrayMap<AccessibilityServicesStateChangeListener, Executor>
             mServicesStateChangeListeners = new ArrayMap<>();
 
@@ -336,7 +358,7 @@
          *
          * @param manager The manager that is calling back
          */
-        void onAccessibilityServicesStateChanged(@NonNull  AccessibilityManager manager);
+        void onAccessibilityServicesStateChanged(@NonNull AccessibilityManager manager);
     }
 
     /**
@@ -358,6 +380,21 @@
     }
 
     /**
+     * Listener for the UI contrast. To listen for changes to
+     * the UI contrast on the device, implement this interface and
+     * register it with the system by calling {@link #addUiContrastChangeListener}.
+     */
+    public interface UiContrastChangeListener {
+
+        /**
+         * Called when the color contrast enabled state changes.
+         *
+         * @param uiContrast The color contrast as in {@link #getUiContrast}
+         */
+        void onUiContrastChanged(@FloatRange(from = -1.0f, to = 1.0f) float uiContrast);
+    }
+
+    /**
      * Listener for the audio description by default state. To listen for
      * changes to the audio description by default state on the device,
      * implement this interface and register it with the system by calling
@@ -471,6 +508,16 @@
                 updateFocusAppearanceLocked(strokeWidth, color);
             }
         }
+
+        @Override
+        public void setUiContrast(float contrast) {
+            synchronized (mLock) {
+                // if value changed in the settings, update the cached value and notify listeners
+                if (Math.abs(mUiContrast - contrast) < 1e-10) return;
+                mUiContrast = contrast;
+            }
+            mHandler.obtainMessage(MyCallback.MSG_NOTIFY_CONTRAST_CHANGED).sendToTarget();
+        }
     };
 
     /**
@@ -641,7 +688,7 @@
     /**
      * Returns if the high text contrast in the system is enabled.
      * <p>
-     * <strong>Note:</strong> You need to query this only if you application is
+     * <strong>Note:</strong> You need to query this only if your application is
      * doing its own rendering and does not rely on the platform rendering pipeline.
      * </p>
      *
@@ -661,6 +708,24 @@
     }
 
     /**
+     * Returns the color contrast for the user.
+     * <p>
+     * <strong>Note:</strong> You need to query this only if your application is
+     * doing its own rendering and does not rely on the platform rendering pipeline.
+     * </p>
+     * @return The color contrast, float in [-1, 1] where
+     *          0 corresponds to the default contrast
+     *         -1 corresponds to the minimum contrast that the user can set
+     *          1 corresponds to the maximum contrast that the user can set
+     */
+    @FloatRange(from = -1.0f, to = 1.0f)
+    public float getUiContrast() {
+        synchronized (mLock) {
+            return mUiContrast;
+        }
+    }
+
+    /**
      * Sends an {@link AccessibilityEvent}.
      *
      * @param event The event to send.
@@ -1240,6 +1305,35 @@
     }
 
     /**
+     * Registers a {@link UiContrastChangeListener} for the current user.
+     *
+     * @param executor The executor on which the listener should be called back.
+     * @param listener The listener.
+     */
+    public void addUiContrastChangeListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull UiContrastChangeListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mLock) {
+            mUiContrastChangeListeners.put(listener, executor);
+        }
+    }
+
+    /**
+     * Unregisters a {@link UiContrastChangeListener} for the current user.
+     * If the listener was not registered, does nothing and returns.
+     *
+     * @param listener The listener to unregister.
+     */
+    public void removeUiContrastChangeListener(@NonNull UiContrastChangeListener listener) {
+        Objects.requireNonNull(listener);
+        synchronized (mLock) {
+            mUiContrastChangeListeners.remove(listener);
+        }
+    }
+
+    /**
      * Registers a {@link AudioDescriptionRequestedChangeListener}
      * for changes in the audio description by default state of the system.
      * The value could be read via {@link #isAudioDescriptionRequested}.
@@ -2004,6 +2098,7 @@
             mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents);
             updateUiTimeout(service.getRecommendedTimeoutMillis());
             updateFocusAppearanceLocked(service.getFocusStrokeWidth(), service.getFocusColor());
+            mUiContrast = service.getUiContrast();
             mService = service;
         } catch (RemoteException re) {
             Log.e(LOG_TAG, "AccessibilityManagerService is dead", re);
@@ -2082,6 +2177,22 @@
     }
 
     /**
+     * Notifies the registered {@link UiContrastChangeListener}s if the value changed.
+     */
+    private void notifyUiContrastChanged() {
+        final ArrayMap<UiContrastChangeListener, Executor> listeners;
+        synchronized (mLock) {
+            listeners = new ArrayMap<>(mUiContrastChangeListeners);
+        }
+
+        listeners.entrySet().forEach(entry -> {
+            UiContrastChangeListener listener = entry.getKey();
+            Executor executor = entry.getValue();
+            executor.execute(() -> listener.onUiContrastChanged(mUiContrast));
+        });
+    }
+
+    /**
      * Notifies the registered {@link AudioDescriptionStateChangeListener}s.
      */
     private void notifyAudioDescriptionbyDefaultStateChanged() {
@@ -2171,6 +2282,7 @@
 
     private final class MyCallback implements Handler.Callback {
         public static final int MSG_SET_STATE = 1;
+        public static final int MSG_NOTIFY_CONTRAST_CHANGED = 2;
 
         @Override
         public boolean handleMessage(Message message) {
@@ -2182,6 +2294,9 @@
                         setStateLocked(state);
                     }
                 } break;
+                case MSG_NOTIFY_CONTRAST_CHANGED: {
+                    notifyUiContrastChanged();
+                }
             }
             return true;
         }
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 364c7c8..c2d899a 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -118,4 +118,6 @@
 
     // Used by UiAutomation for tests on the InputFilter
     void injectInputEventToInputFilter(in InputEvent event);
+
+    float getUiContrast();
 }
diff --git a/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl
index 041399c..931f862 100644
--- a/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl
@@ -31,4 +31,6 @@
     void setRelevantEventTypes(int eventTypes);
 
     void setFocusAppearance(int strokeWidth, int color);
+
+    void setUiContrast(float contrast);
 }
diff --git a/core/java/android/view/inputmethod/TextBoundsInfo.java b/core/java/android/view/inputmethod/TextBoundsInfo.java
index 4e87405..dd05543 100644
--- a/core/java/android/view/inputmethod/TextBoundsInfo.java
+++ b/core/java/android/view/inputmethod/TextBoundsInfo.java
@@ -25,6 +25,7 @@
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.text.Layout;
 import android.text.SegmentFinder;
 
 import com.android.internal.util.ArrayUtils;
@@ -314,6 +315,554 @@
     }
 
     /**
+     * Return the index of the closest character to the given position.
+     * It's similar to the text layout API {@link Layout#getOffsetForHorizontal(int, float)}.
+     * And it's mainly used to find the cursor index (the index of the character before which the
+     * cursor should be placed) for the given position. It's guaranteed that the returned index is
+     * a grapheme break. Check {@link #getGraphemeSegmentFinder()} for more information.
+     *
+     * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom and each
+     * line is laid out according to the display algorithm specified in
+     * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
+     * algorithm</a>.
+     * </p>
+     *
+     * <p> This method won't check the text ranges whose line information is missing. For example,
+     * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated
+     * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
+     * won't check the text in the ranges of [5, 7) and [12, 15).
+     * </p>
+     *
+     * @param x the x coordinates of the interested location, in the editor's coordinates.
+     * @param y the y coordinates of the interested location, in the editor's coordinates.
+     * @return the index of the character whose position is closest to the given location. It will
+     * return -1 if it can't find a character.
+     *
+     * @see Layout#getOffsetForHorizontal(int, float)
+     */
+    public int getOffsetForPosition(float x, float y) {
+        final int[] lineRange = new int[2];
+        final RectF lineBounds = new RectF();
+        getLineInfo(y, lineRange, lineBounds);
+        // No line is found, return -1;
+        if (lineRange[0] == -1 || lineRange[1] == -1) return -1;
+        final int lineStart = lineRange[0];
+        final int lineEnd = lineRange[1];
+
+        final boolean lineEndsWithLinefeed =
+                (getCharacterFlags(lineEnd - 1) & FLAG_CHARACTER_LINEFEED) != 0;
+
+        // Consider the following 2 cases:
+        // Case 1:
+        //   Text: "AB\nCD"
+        //   Layout: AB
+        //           CD
+        // Case 2:
+        //   Text: "ABCD"
+        //   Layout: AB
+        //           CD
+        // If user wants to insert a 'X' character at the end of the first line:
+        //   In case 1, 'X' is inserted before the last character '\n'.
+        //   In case 2, 'X' is inserted after the last character 'B'.
+        // So if a line ends with linefeed, it shouldn't check the cursor position after the last
+        // character.
+        final int lineLimit;
+        if (lineEndsWithLinefeed) {
+            lineLimit = lineEnd;
+        } else {
+            lineLimit = lineEnd + 1;
+        }
+        // Point graphemeStart to the start of the first grapheme segment intersects with the line.
+        int graphemeStart = mGraphemeSegmentFinder.nextEndBoundary(lineStart);
+        // The grapheme information is missing.
+        if (graphemeStart == SegmentFinder.DONE) return -1;
+        graphemeStart = mGraphemeSegmentFinder.previousStartBoundary(graphemeStart);
+
+        int target = -1;
+        float minDistance = Float.MAX_VALUE;
+        while (graphemeStart != SegmentFinder.DONE && graphemeStart < lineLimit) {
+            if (graphemeStart >= lineStart) {
+                float cursorPosition = getCursorHorizontalPosition(graphemeStart, lineStart,
+                        lineEnd, lineBounds.left, lineBounds.right);
+                final float distance = Math.abs(cursorPosition - x);
+                if (distance < minDistance) {
+                    minDistance = distance;
+                    target = graphemeStart;
+                }
+            }
+            graphemeStart = mGraphemeSegmentFinder.nextStartBoundary(graphemeStart);
+        }
+
+        return target;
+    }
+
+    /**
+     * Whether the primary position at the given index is the previous character's trailing
+     * position. <br/>
+     *
+     * For LTR character, trailing position is its right edge. For RTL character, trailing position
+     * is its left edge.
+     *
+     * The primary position is defined as the position of a newly inserted character with the
+     * context direction at the given offset. In contrast, the secondary position is the position
+     * of a newly inserted character with the context's opposite direction at the given offset.
+     *
+     * In Android, the trailing position is used for primary position when the direction run after
+     * the given index has a higher level than the current direction run.
+     *
+     * <p>
+     * For example:
+     * (L represents LTR character, and R represents RTL character. The number is the index)
+     * <pre>
+     * input text:          L0 L1 L2 R3 R4 R5 L6 L7 L8
+     * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
+     * BiDi Run:            [ Run 0 ][ Run 1 ][ Run 2 ]
+     * BiDi Level:          0  0  0  1  1  1  0  0  0
+     * </pre>
+     *
+     * The index 3 is a BiDi transition point, the cursor can be placed either after L2 or before
+     * R3. Because the bidi level of run 1 is higher than the run 0, this method returns true. And
+     * the cursor should be placed after L2.
+     * <pre>
+     * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
+     * position after L2:           |
+     * position before R3:                   |
+     * result position:             |
+     * </pre>
+     *
+     * The index 6 is also a Bidi transition point, the 2 possible cursor positions are exactly the
+     * same as index 3. However, since the bidi level of run 2 is higher than the run 1, this
+     * method returns false. And the cursor should be placed before L6.
+     * <pre>
+     * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
+     * position after R5:           |
+     * position before L6:                   |
+     * result position:                      |
+     * </pre>
+     *
+     * This method helps guarantee that the cursor index and the cursor position forms a one to
+     * one relation.
+     * </p>
+     *
+     * @param offset the offset of the character in front of which the cursor is placed. It must be
+     *              the start index of a grapheme. And it must be in the range from lineStart to
+     *              lineEnd. An offset equal to lineEnd is allowed. It indicates that the cursor is
+     *              placed at the end of current line instead of the start of the following line.
+     * @param lineStart the start index of the line that index belongs to, inclusive.
+     * @param lineEnd the end index of the line that index belongs to, exclusive.
+     * @return true if primary position is the trailing position of the previous character.
+     *
+     * @see #getCursorHorizontalPosition(int, int, int, float, float)
+     */
+    private boolean primaryIsTrailingPrevious(int offset, int lineStart, int lineEnd) {
+        final int bidiLevel;
+        if (offset < lineEnd) {
+            bidiLevel = getCharacterBidiLevel(offset);
+        } else {
+            // index equals to lineEnd, use line's BiDi level for the BiDi run.
+            boolean lineIsRtl =
+                    (getCharacterFlags(offset - 1) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
+            bidiLevel = lineIsRtl ? 1 : 0;
+        }
+        final int bidiLevelBefore;
+        if (offset > lineStart) {
+            // Here it assumes index is always the start of a grapheme. And (index - 1) belongs to
+            // the previous grapheme.
+            bidiLevelBefore = getCharacterBidiLevel(offset - 1);
+        } else {
+            // index equals to lineStart, use line's BiDi level for previous BiDi run.
+            boolean lineIsRtl =
+                    (getCharacterFlags(offset) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
+            bidiLevelBefore = lineIsRtl ? 1 : 0;
+        }
+        return bidiLevelBefore < bidiLevel;
+    }
+
+    /**
+     * Returns the x coordinates of the cursor at the given index. (The index of the character
+     * before which the cursor should be placed.)
+     *
+     * @param index the character index before which the cursor is placed. It must be the start
+     *              index of a grapheme. It must be in the range from lineStart to lineEnd.
+     *              An index equal to lineEnd is allowed. It indicates that the cursor is
+     *              placed at the end of current line instead of the start of the following line.
+     * @param lineStart start index of the line that index belongs to, inclusive.
+     * @param lineEnd end index of the line that index belongs, exclusive.
+     * @return the x coordinates of the cursor at the given index,
+     *
+     * @see #primaryIsTrailingPrevious(int, int, int)
+     */
+    private float getCursorHorizontalPosition(int index, int lineStart, int lineEnd,
+            float lineLeft, float lineRight) {
+        Preconditions.checkArgumentInRange(index, lineStart, lineEnd, "index");
+        final boolean lineIsRtl = (getCharacterFlags(lineStart) & FLAG_LINE_IS_RTL) != 0;
+        final boolean isPrimaryIsTrailingPrevious =
+                primaryIsTrailingPrevious(index, lineStart, lineEnd);
+
+        // The index of the character used to compute the cursor position.
+        final int targetIndex;
+        // Whether to use the start position of the character.
+        // For LTR character start is the left edge. For RTL character, start is the right edge.
+        final boolean isStart;
+        if (isPrimaryIsTrailingPrevious) {
+            // (index - 1) belongs to the previous line(if any), return the line start position.
+            if (index <= lineStart) {
+                return lineIsRtl ? lineRight : lineLeft;
+            }
+            targetIndex = index - 1;
+            isStart = false;
+        } else {
+            // index belongs to the next line(if any), return the line end position.
+            if (index >= lineEnd) {
+                return lineIsRtl ? lineLeft : lineRight;
+            }
+            targetIndex = index;
+            isStart = true;
+        }
+
+        // The BiDi level is odd when the character is RTL.
+        final boolean isRtl = (getCharacterBidiLevel(targetIndex) & 1) != 0;
+        final int offset = targetIndex - mStart;
+        // If the character is RTL, the start is the right edge. Otherwise, the start is the
+        // left edge:
+        //  +-----------------------+
+        //  |       | start | end   |
+        //  |-------+-------+-------|
+        //  | RTL   | right | left  |
+        //  |-------+-------+-------|
+        //  | LTR   | left  | right |
+        //  +-------+-------+-------+
+        return (isRtl != isStart) ? mCharacterBounds[4 * offset] : mCharacterBounds[4 * offset + 2];
+    }
+
+    /**
+     * Return the minimal rectangle that contains all the characters in the given range.
+     *
+     * @param start the start index of the given range, inclusive.
+     * @param end the end index of the given range, exclusive.
+     * @param rectF the {@link RectF} to receive the bounds.
+     */
+    private void getBoundsForRange(int start, int end, @NonNull RectF rectF) {
+        Preconditions.checkArgumentInRange(start, mStart, mEnd - 1, "start");
+        Preconditions.checkArgumentInRange(end, start, mEnd, "end");
+        if (end <= start) {
+            rectF.setEmpty();
+            return;
+        }
+
+        rectF.left = Float.MAX_VALUE;
+        rectF.top = Float.MAX_VALUE;
+        rectF.right = Float.MIN_VALUE;
+        rectF.bottom = Float.MIN_VALUE;
+        for (int index = start; index < end; ++index) {
+            final int offset = index - mStart;
+            rectF.left = Math.min(rectF.left, mCharacterBounds[4 * offset]);
+            rectF.top = Math.min(rectF.top, mCharacterBounds[4 * offset + 1]);
+            rectF.right = Math.max(rectF.right, mCharacterBounds[4 * offset + 2]);
+            rectF.bottom = Math.max(rectF.bottom, mCharacterBounds[4 * offset + 3]);
+        }
+    }
+
+    /**
+     * Return the character range and bounds of the closest line to the given {@code y} coordinate,
+     * in the editor's local coordinates.
+     *
+     * If the given y is above the first line or below the last line -1 will be returned for line
+     * start and end.
+     *
+     * This method assumes that the lines are laid out from the top to bottom.
+     *
+     * @param y the y coordinates used to search for the line.
+     * @param characterRange a two element array used to receive the character range of the line.
+     *                       If no valid line is found -1 will be returned for both start and end.
+     * @param bounds {@link RectF} to receive the line bounds result, nullable. If given, it can
+     *                            still be modified even if no valid line is found.
+     */
+    private void getLineInfo(float y, @NonNull int[] characterRange, @Nullable RectF bounds) {
+        characterRange[0] = -1;
+        characterRange[1] = -1;
+
+        // Starting from the first line.
+        int currentLineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
+        if (currentLineEnd == SegmentFinder.DONE) return;
+        int currentLineStart = mLineSegmentFinder.previousStartBoundary(currentLineEnd);
+
+        float top = Float.MAX_VALUE;
+        float bottom = Float.MIN_VALUE;
+        float minDistance = Float.MAX_VALUE;
+        final RectF currentLineBounds = new RectF();
+        while (currentLineStart != SegmentFinder.DONE && currentLineStart < mEnd) {
+            final int lineStartInRange = Math.max(mStart, currentLineStart);
+            final int lineEndInRange = Math.min(mEnd, currentLineEnd);
+            getBoundsForRange(lineStartInRange, lineEndInRange, currentLineBounds);
+
+            top = Math.min(currentLineBounds.top, top);
+            bottom = Math.max(currentLineBounds.bottom, bottom);
+
+            final float distance = verticalDistance(currentLineBounds, y);
+
+            if (distance == 0f) {
+                characterRange[0] = currentLineStart;
+                characterRange[1] = currentLineEnd;
+                if (bounds != null) {
+                    bounds.set(currentLineBounds);
+                }
+                return;
+            }
+
+            if (distance < minDistance) {
+                minDistance = distance;
+                characterRange[0] = currentLineStart;
+                characterRange[1] = currentLineEnd;
+                if (bounds != null) {
+                    bounds.set(currentLineBounds);
+                }
+            }
+            if (y < bounds.top) break;
+            currentLineStart = mLineSegmentFinder.nextStartBoundary(currentLineStart);
+            currentLineEnd = mLineSegmentFinder.nextEndBoundary(currentLineEnd);
+        }
+
+        // y is above the first line or below the last line. The founded line is still invalid,
+        // clear the result.
+        if (y < top || y > bottom) {
+            characterRange[0] = -1;
+            characterRange[1] = -1;
+            if (bounds != null) {
+                bounds.setEmpty();
+            }
+        }
+    }
+
+    /**
+     * Finds the range of text which is inside the specified rectangle area. This method is a
+     * counterpart of the
+     * {@link Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)}.
+     *
+     * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom
+     * and each line is laid out according to the display algorithm specified in
+     * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
+     * algorithm</a>.
+     * </p>
+     *
+     * <p> This method won't check the text ranges whose line information is missing. For example,
+     * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated line
+     * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
+     * won't check the text in the ranges of [5, 7) and [12, 15).
+     * </p>
+     *
+     * @param area area for which the text range will be found
+     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
+     *     text segment
+     * @param inclusionStrategy strategy for determining whether a text segment is inside the
+     *          specified area
+     * @return the text range stored in a two element int array. The first element is the
+     * start (inclusive) of the text range, and the second element is the end (exclusive) character
+     * offsets of the text range, or null if there are no text segments inside the area.
+     *
+     * @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)
+     */
+    @Nullable
+    public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
+            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+        int lineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
+        // Line information is missing.
+        if (lineEnd == SegmentFinder.DONE) return null;
+        int lineStart = mLineSegmentFinder.previousStartBoundary(lineEnd);
+
+        int start = -1;
+        while (lineStart != SegmentFinder.DONE && start == -1) {
+            start = getStartForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
+                    inclusionStrategy);
+            lineStart = mLineSegmentFinder.nextStartBoundary(lineStart);
+            lineEnd = mLineSegmentFinder.nextEndBoundary(lineEnd);
+        }
+
+        // Can't find the start index; the specified contains no valid segment.
+        if (start == -1) return null;
+
+        lineStart = mLineSegmentFinder.previousStartBoundary(mEnd);
+        // Line information is missing.
+        if (lineStart == SegmentFinder.DONE) return null;
+        lineEnd = mLineSegmentFinder.nextEndBoundary(lineStart);
+        int end = -1;
+        while (lineEnd > start && end == -1) {
+            end = getEndForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
+                    inclusionStrategy);
+            lineStart = mLineSegmentFinder.previousStartBoundary(lineStart);
+            lineEnd = mLineSegmentFinder.previousEndBoundary(lineEnd);
+        }
+
+        // We've already found start, end is guaranteed to be found at this point.
+        start = segmentFinder.previousStartBoundary(start + 1);
+        end = segmentFinder.nextEndBoundary(end - 1);
+        return new int[] { start, end };
+    }
+
+    /**
+     * Find the start character index of the first text segments within a line inside the specified
+     * {@code area}.
+     *
+     * @param lineStart the start of this line, inclusive .
+     * @param lineEnd the end of this line, exclusive.
+     * @param area the area inside which the text segments will be found.
+     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
+     *                      text segment.
+     * @param inclusionStrategy strategy for determining whether a text segment is inside the
+     *                          specified area.
+     * @return the start index of the first segment in the area.
+     */
+    private int getStartForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
+            @NonNull SegmentFinder segmentFinder,
+            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+        if (lineStart >= lineEnd) return -1;
+
+        int runStart = lineStart;
+        int runLevel = -1;
+        // Check the BiDi runs and search for the start index.
+        for (int index = lineStart; index < lineEnd; ++index) {
+            final int level = getCharacterBidiLevel(index);
+            if (level != runLevel) {
+                final int start = getStartForRectWithinRun(runStart, index, area, segmentFinder,
+                        inclusionStrategy);
+                if (start != -1) {
+                    return start;
+                }
+
+                runStart = index;
+                runLevel = level;
+            }
+        }
+        return getStartForRectWithinRun(runStart, lineEnd, area, segmentFinder, inclusionStrategy);
+    }
+
+    /**
+     * Find the start character index of the first text segments within the directional run inside
+     * the specified {@code area}.
+     *
+     * @param runStart the start of this directional run, inclusive.
+     * @param runEnd the end of this directional run, exclusive.
+     * @param area the area inside which the text segments will be found.
+     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
+     *                      text segment.
+     * @param inclusionStrategy strategy for determining whether a text segment is inside the
+     *                          specified area.
+     * @return the start index of the first segment in the area.
+     */
+    private int getStartForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
+            @NonNull SegmentFinder segmentFinder,
+            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+        if (runStart >= runEnd) return -1;
+
+        int segmentEndOffset = segmentFinder.nextEndBoundary(runStart);
+        // No segment is found in run.
+        if (segmentEndOffset == SegmentFinder.DONE) return -1;
+        int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);
+
+        final RectF segmentBounds = new RectF();
+        while (segmentStartOffset != SegmentFinder.DONE && segmentStartOffset < runEnd) {
+            final int start = Math.max(runStart, segmentStartOffset);
+            final int end = Math.min(runEnd, segmentEndOffset);
+            getBoundsForRange(start, end, segmentBounds);
+            // Find the first segment inside the area, return the start.
+            if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return start;
+
+            segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset);
+            segmentEndOffset = segmentFinder.nextEndBoundary(segmentEndOffset);
+        }
+        return -1;
+    }
+
+    /**
+     * Find the end character index of the last text segments within a line inside the specified
+     * {@code area}.
+     *
+     * @param lineStart the start of this line, inclusive .
+     * @param lineEnd the end of this line, exclusive.
+     * @param area the area inside which the text segments will be found.
+     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
+     *                      text segment.
+     * @param inclusionStrategy strategy for determining whether a text segment is inside the
+     *                          specified area.
+     * @return the end index of the last segment in the area.
+     */
+    private int getEndForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
+            @NonNull SegmentFinder segmentFinder,
+            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+        if (lineStart >= lineEnd) return -1;
+        lineStart = Math.max(lineStart, mStart);
+        lineEnd = Math.min(lineEnd, mEnd);
+
+        // The exclusive run end index.
+        int runEnd = lineEnd;
+        int runLevel = -1;
+        // Check the BiDi runs backwards and search for the end index.
+        for (int index = lineEnd - 1; index >= lineStart; --index) {
+            final int level = getCharacterBidiLevel(index);
+            if (level != runLevel) {
+                final int end = getEndForRectWithinRun(index + 1, runEnd, area, segmentFinder,
+                        inclusionStrategy);
+                if (end != -1) return end;
+
+                runEnd = index + 1;
+                runLevel = level;
+            }
+        }
+        return getEndForRectWithinRun(lineStart, runEnd, area, segmentFinder, inclusionStrategy);
+    }
+
+    /**
+     * Find the end character index of the last text segments within the directional run inside the
+     * specified {@code area}.
+     *
+     * @param runStart the start of this directional run, inclusive.
+     * @param runEnd the end of this directional run, exclusive.
+     * @param area the area inside which the text segments will be found.
+     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
+     *                      text segment.
+     * @param inclusionStrategy strategy for determining whether a text segment is inside the
+     *                          specified area.
+     * @return the end index of the last segment in the area.
+     */
+    private int getEndForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
+            @NonNull SegmentFinder segmentFinder,
+            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+        if (runStart >= runEnd) return -1;
+
+        int segmentStart = segmentFinder.previousStartBoundary(runEnd);
+        // No segment is found before the runEnd.
+        if (segmentStart == SegmentFinder.DONE) return -1;
+        int segmentEnd = segmentFinder.nextEndBoundary(segmentStart);
+
+        final RectF segmentBounds = new RectF();
+        while (segmentEnd != SegmentFinder.DONE && segmentEnd > runStart) {
+            final int start = Math.max(runStart, segmentStart);
+            final int end = Math.min(runEnd, segmentEnd);
+            getBoundsForRange(start, end, segmentBounds);
+            // Find the last segment inside the area, return the end.
+            if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return end;
+
+            segmentStart = segmentFinder.previousStartBoundary(segmentStart);
+            segmentEnd = segmentFinder.previousEndBoundary(segmentEnd);
+        }
+        return -1;
+    }
+
+    /**
+     * Get the vertical distance from the {@code pointF} to the {@code rectF}. It's useful to find
+     * the corresponding line for a given point.
+     */
+    private static float verticalDistance(@NonNull RectF rectF, float y) {
+        if (rectF.top <= y && y < rectF.bottom) {
+            return 0f;
+        }
+        if (y < rectF.top) {
+            return rectF.top - y;
+        }
+        return y - rectF.bottom;
+    }
+
+    /**
      * Describe the kinds of special objects contained in this Parcelable
      * instance's marshaled representation. For example, if the object will
      * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java
index 3950739..250652a 100644
--- a/core/java/android/window/SurfaceSyncGroup.java
+++ b/core/java/android/window/SurfaceSyncGroup.java
@@ -40,64 +40,65 @@
  * mechanism so each sync implementation doesn't need to handle it themselves. The SurfaceSyncGroup
  * class is used the following way.
  *
- * 1. {@link #SurfaceSyncGroup()} constructor is called
- * 2. {@link #addToSync(SyncTarget)} is called for every SyncTarget object that wants to be
- * included in the sync. If the addSync is called for an {@link AttachedSurfaceControl} or
- * {@link SurfaceView} it needs to be called on the UI thread. When addToSync is called, it's
+ * 1. {@link #addToSync(SurfaceSyncGroup, boolean)} is called for every SurfaceSyncGroup object that
+ * wants to be included in the sync. If the addSync is called for an {@link AttachedSurfaceControl}
+ * or {@link SurfaceView} it needs to be called on the UI thread. When addToSync is called, it's
  * guaranteed that any UI updates that were requested before addToSync but after the last frame
  * drew, will be included in the sync.
- * 3. {@link #markSyncReady()} should be called when all the {@link SyncTarget}s have been added
- * to the SurfaceSyncGroup. At this point, the SurfaceSyncGroup is closed and no more SyncTargets
- * can be added to it.
- * 4. The SurfaceSyncGroup will gather the data for each SyncTarget using the steps described below.
- * When all the SyncTargets have finished, the syncRequestComplete will be invoked and the
- * transaction will either be applied or sent to the caller. In most cases, only the
- * SurfaceSyncGroup  should be handling the Transaction object directly. However, there are some
+ * 2. {@link #markSyncReady()} should be called when all the {@link SurfaceSyncGroup}s have been
+ * added to the SurfaceSyncGroup. At this point, the SurfaceSyncGroup is closed and no more
+ * SurfaceSyncGroups can be added to it.
+ * 3. The SurfaceSyncGroup will gather the data for each SurfaceSyncGroup using the steps described
+ * below. When all the SurfaceSyncGroups have finished, the syncRequestComplete will be invoked and
+ * the transaction will either be applied or sent to the caller. In most cases, only the
+ * SurfaceSyncGroup should be handling the Transaction object directly. However, there are some
  * cases where the framework needs to send the Transaction elsewhere, like in ViewRootImpl, so that
  * option is provided.
  *
- * The following is what happens within the {@link SurfaceSyncGroup}
- * 1. Each SyncTarget will get a {@link SyncTarget#onAddedToSyncGroup} callback that contains a
- * {@link TransactionReadyCallback}.
- * 2. Each {@link SyncTarget} needs to invoke
- * {@link TransactionReadyCallback#onTransactionReady(Transaction)}. This makes sure the
- * SurfaceSyncGroup knows when the SyncTarget is complete, allowing the SurfaceSyncGroup to get the
- * Transaction that contains the buffer.
- * 3. When the final TransactionReadyCallback finishes for the SurfaceSyncGroup, in most cases the
- * transaction is applied and then the sync complete callbacks are invoked, letting the callers know
- * the sync is now complete.
+ * The following is what happens within the {@link android.window.SurfaceSyncGroup}
+ * 1. Each SurfaceSyncGroup will get a
+ * {@link SurfaceSyncGroup#onAddedToSyncGroup(SurfaceSyncGroup, TransactionReadyCallback)} callback
+ * that contains a  {@link TransactionReadyCallback}.
+ * 2. Each {@link SurfaceSyncGroup} needs to invoke
+ * {@link SurfaceSyncGroup#onTransactionReady(Transaction)}.
+ * This makes sure the parent SurfaceSyncGroup knows when the SurfaceSyncGroup is complete, allowing
+ * the parent SurfaceSyncGroup to get the Transaction that contains the changes for the child
+ * SurfaceSyncGroup
+ * 3. When the final TransactionReadyCallback finishes for the child SurfaceSyncGroups, the
+ * transaction is either applied if it's the top most parent or the final merged transaction is sent
+ * up to its parent SurfaceSyncGroup.
  *
  * @hide
  */
-public final class SurfaceSyncGroup {
+public class SurfaceSyncGroup {
     private static final String TAG = "SurfaceSyncGroup";
     private static final boolean DEBUG = false;
 
     private static Supplier<Transaction> sTransactionFactory = Transaction::new;
 
     /**
-     * Class that collects the {@link SyncTarget}s and notifies when all the surfaces have
+     * Class that collects the {@link SurfaceSyncGroup}s and notifies when all the surfaces have
      * a frame ready.
      */
     private final Object mLock = new Object();
 
     @GuardedBy("mLock")
-    private final Set<Integer> mPendingSyncs = new ArraySet<>();
+    private final Set<TransactionReadyCallback> mPendingSyncs = new ArraySet<>();
     @GuardedBy("mLock")
     private final Transaction mTransaction = sTransactionFactory.get();
     @GuardedBy("mLock")
     private boolean mSyncReady;
 
     @GuardedBy("mLock")
-    private Consumer<Transaction> mSyncRequestCompleteCallback;
-
-    @GuardedBy("mLock")
-    private final Set<SurfaceSyncGroup> mMergedSyncGroups = new ArraySet<>();
-
-    @GuardedBy("mLock")
     private boolean mFinished;
 
     @GuardedBy("mLock")
+    private TransactionReadyCallback mTransactionReadyCallback;
+
+    @GuardedBy("mLock")
+    private SurfaceSyncGroup mParentSyncGroup;
+
+    @GuardedBy("mLock")
     private final ArraySet<Pair<Executor, Runnable>> mSyncCompleteCallbacks = new ArraySet<>();
 
     /**
@@ -122,16 +123,16 @@
     /**
      * Creates a sync.
      *
-     * @param syncRequestComplete The complete callback that contains the syncId and transaction
-     *                            with all the sync data merged. The Transaction passed back can be
-     *                            null.
+     * @param transactionReadyCallback The complete callback that contains the syncId and
+     *                                 transaction with all the sync data merged. The Transaction
+     *                                 passed back can be null.
      *
      * NOTE: Only should be used by ViewRootImpl
      * @hide
      */
-    public SurfaceSyncGroup(Consumer<Transaction> syncRequestComplete) {
-        mSyncRequestCompleteCallback = transaction -> {
-            syncRequestComplete.accept(transaction);
+    public SurfaceSyncGroup(Consumer<Transaction> transactionReadyCallback) {
+        mTransactionReadyCallback = transaction -> {
+            transactionReadyCallback.accept(transaction);
             synchronized (mLock) {
                 for (Pair<Executor, Runnable> callback : mSyncCompleteCallbacks) {
                     callback.first.execute(callback.second);
@@ -157,6 +158,31 @@
     }
 
     /**
+     * Mark the sync set as ready to complete. No more data can be added to the specified
+     * syncId.
+     * Once the sync set is marked as ready, it will be able to complete once all Syncables in the
+     * set have completed their sync
+     */
+    public void markSyncReady() {
+        onTransactionReady(null);
+    }
+
+    /**
+     * Similar to {@link #markSyncReady()}, but a transaction is passed in to merge with the
+     * SurfaceSyncGroup.
+     * @param t The transaction that merges into the main Transaction for the SurfaceSyncGroup.
+     */
+    public void onTransactionReady(@Nullable Transaction t) {
+        synchronized (mLock) {
+            mSyncReady = true;
+            if (t != null) {
+                mTransaction.merge(t);
+            }
+            checkIfSyncIsComplete();
+        }
+    }
+
+    /**
      * Add a SurfaceView to a sync set. This is different than
      * {@link #addToSync(AttachedSurfaceControl)} because it requires the caller to notify the start
      * and finish drawing in order to sync.
@@ -171,7 +197,13 @@
     @UiThread
     public boolean addToSync(SurfaceView surfaceView,
             Consumer<SurfaceViewFrameCallback> frameCallbackConsumer) {
-        return addToSync(new SurfaceViewSyncTarget(surfaceView, frameCallbackConsumer));
+        SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup();
+        if (addToSync(surfaceSyncGroup, false /* parentSyncGroupMerge */)) {
+            frameCallbackConsumer.accept(
+                    () -> surfaceView.syncNextFrame(surfaceSyncGroup::onTransactionReady));
+            return true;
+        }
+        return false;
     }
 
     /**
@@ -185,29 +217,38 @@
         if (viewRoot == null) {
             return false;
         }
-        SyncTarget syncTarget = viewRoot.getSyncTarget();
-        if (syncTarget == null) {
+        SurfaceSyncGroup surfaceSyncGroup = viewRoot.getOrCreateSurfaceSyncGroup();
+        if (surfaceSyncGroup == null) {
             return false;
         }
-        return addToSync(syncTarget);
+        return addToSync(surfaceSyncGroup, false /* parentSyncGroupMerge */);
     }
 
     /**
-     * Add a {@link SyncTarget} to a sync set. The sync set will wait for all
+     * Add a {@link SurfaceSyncGroup} to a sync set. The sync set will wait for all
      * SyncableSurfaces to complete before notifying.
      *
-     * @param syncTarget A SyncTarget that implements how to handle syncing transactions.
-     * @return true if the SyncTarget was successfully added to the SyncGroup, false otherwise.
+     * @param surfaceSyncGroup A SyncableSurface that implements how to handle syncing
+     *                         buffers.
+     * @return true if the SyncGroup was successfully added to the current SyncGroup, false
+     * otherwise.
      */
-    public boolean addToSync(SyncTarget syncTarget) {
+    public boolean addToSync(SurfaceSyncGroup surfaceSyncGroup, boolean parentSyncGroupMerge) {
         TransactionReadyCallback transactionReadyCallback = new TransactionReadyCallback() {
             @Override
             public void onTransactionReady(Transaction t) {
                 synchronized (mLock) {
                     if (t != null) {
+                        // When an older parent sync group is added due to a child syncGroup getting
+                        // added to multiple groups, we need to maintain merge order so the older
+                        // parentSyncGroup transactions are overwritten by anything in the newer
+                        // parentSyncGroup.
+                        if (parentSyncGroupMerge) {
+                            t.merge(mTransaction);
+                        }
                         mTransaction.merge(t);
                     }
-                    mPendingSyncs.remove(hashCode());
+                    mPendingSyncs.remove(this);
                     checkIfSyncIsComplete();
                 }
             }
@@ -216,48 +257,16 @@
         synchronized (mLock) {
             if (mSyncReady) {
                 Log.e(TAG, "Sync " + this + " was already marked as ready. No more "
-                        + "SyncTargets can be added.");
+                        + "SurfaceSyncGroups can be added.");
                 return false;
             }
-            mPendingSyncs.add(transactionReadyCallback.hashCode());
+            mPendingSyncs.add(transactionReadyCallback);
         }
-        syncTarget.onAddedToSyncGroup(this, transactionReadyCallback);
+        surfaceSyncGroup.onAddedToSyncGroup(this, transactionReadyCallback);
         return true;
     }
 
     /**
-     * Mark the sync set as ready to complete. No more data can be added to the specified
-     * syncId.
-     * Once the sync set is marked as ready, it will be able to complete once all Syncables in the
-     * set have completed their sync
-     */
-    public void markSyncReady() {
-        synchronized (mLock) {
-            mSyncReady = true;
-            checkIfSyncIsComplete();
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void checkIfSyncIsComplete() {
-        if (!mSyncReady || !mPendingSyncs.isEmpty() || !mMergedSyncGroups.isEmpty()) {
-            if (DEBUG) {
-                Log.d(TAG, "Syncable is not complete. mSyncReady=" + mSyncReady
-                        + " mPendingSyncs=" + mPendingSyncs.size() + " mergedSyncs="
-                        + mMergedSyncGroups.size());
-            }
-            return;
-        }
-
-        if (DEBUG) {
-            Log.d(TAG, "Successfully finished sync id=" + this);
-        }
-
-        mSyncRequestCompleteCallback.accept(mTransaction);
-        mFinished = true;
-    }
-
-    /**
      * Add a Transaction to this sync set. This allows the caller to provide other info that
      * should be synced with the transactions.
      */
@@ -267,99 +276,68 @@
         }
     }
 
-    private void updateCallback(Consumer<Transaction> transactionConsumer) {
+    @GuardedBy("mLock")
+    private void checkIfSyncIsComplete() {
+        if (mFinished) {
+            if (DEBUG) {
+                Log.d(TAG, "SurfaceSyncGroup=" + this + " is already complete");
+            }
+            return;
+        }
+
+        if (!mSyncReady || !mPendingSyncs.isEmpty()) {
+            if (DEBUG) {
+                Log.d(TAG, "SurfaceSyncGroup=" + this + " is not complete. mSyncReady="
+                        + mSyncReady + " mPendingSyncs=" + mPendingSyncs.size());
+            }
+            return;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Successfully finished sync id=" + this);
+        }
+        mTransactionReadyCallback.onTransactionReady(mTransaction);
+        mFinished = true;
+    }
+
+    private void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
+            TransactionReadyCallback transactionReadyCallback) {
+        boolean finished = false;
         synchronized (mLock) {
             if (mFinished) {
-                Log.e(TAG, "Attempting to merge SyncGroup " + this + " when sync is"
-                        + " already complete");
-                transactionConsumer.accept(null);
-            }
-
-            final Consumer<Transaction> oldCallback = mSyncRequestCompleteCallback;
-            mSyncRequestCompleteCallback = transaction -> {
-                oldCallback.accept(null);
-                transactionConsumer.accept(transaction);
-            };
-        }
-    }
-
-    /**
-     * Merge a SyncGroup into this SyncGroup. Since SyncGroups could still have pending SyncTargets,
-     * we need to make sure those can still complete before the mergeTo SyncGroup is considered
-     * complete.
-     *
-     * We keep track of all the merged SyncGroups until they are marked as done, and then they
-     * are removed from the set. This SyncGroup is not considered done until all the merged
-     * SyncGroups are done.
-     *
-     * When the merged SyncGroup is complete, it will invoke the original syncRequestComplete
-     * callback but send an empty transaction to ensure the changes are applied early. This
-     * is needed in case the original sync is relying on the callback to continue processing.
-     *
-     * @param otherSyncGroup The other SyncGroup to merge into this one.
-     */
-    public void merge(SurfaceSyncGroup otherSyncGroup) {
-        synchronized (mLock) {
-            mMergedSyncGroups.add(otherSyncGroup);
-        }
-        otherSyncGroup.updateCallback(transaction -> {
-            synchronized (mLock) {
-                mMergedSyncGroups.remove(otherSyncGroup);
-                if (transaction != null) {
-                    mTransaction.merge(transaction);
+                finished = true;
+            } else {
+                // If this SurfaceSyncGroup was already added to a different SurfaceSyncGroup, we
+                // need to combine everything. We can add the old SurfaceSyncGroup parent to the new
+                // parent so the new parent doesn't complete until the old parent does.
+                // Additionally, the old parent will not get the final transaction object and
+                // instead will send it to the new parent, ensuring that any other SurfaceSyncGroups
+                // from the original parent are also combined with the new parent SurfaceSyncGroup.
+                if (mParentSyncGroup != null) {
+                    Log.d(TAG, "Already part of sync group " + mParentSyncGroup + " " + this);
+                    parentSyncGroup.addToSync(mParentSyncGroup, true /* parentSyncGroupMerge */);
                 }
-                checkIfSyncIsComplete();
+                mParentSyncGroup = parentSyncGroup;
+                final TransactionReadyCallback lastCallback = mTransactionReadyCallback;
+                mTransactionReadyCallback = t -> {
+                    lastCallback.onTransactionReady(null);
+                    transactionReadyCallback.onTransactionReady(t);
+                };
             }
-        });
-    }
-
-    /**
-     * Wrapper class to help synchronize SurfaceViews
-     */
-    private static class SurfaceViewSyncTarget implements SyncTarget {
-        private final SurfaceView mSurfaceView;
-        private final Consumer<SurfaceViewFrameCallback> mFrameCallbackConsumer;
-
-        SurfaceViewSyncTarget(SurfaceView surfaceView,
-                Consumer<SurfaceViewFrameCallback> frameCallbackConsumer) {
-            mSurfaceView = surfaceView;
-            mFrameCallbackConsumer = frameCallbackConsumer;
         }
 
-        @Override
-        public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
-                TransactionReadyCallback transactionReadyCallback) {
-            mFrameCallbackConsumer.accept(
-                    () -> mSurfaceView.syncNextFrame(transactionReadyCallback::onTransactionReady));
+        // Invoke the callback outside of the lock when the SurfaceSyncGroup being added was already
+        // complete.
+        if (finished) {
+            transactionReadyCallback.onTransactionReady(null);
         }
     }
-
-    /**
-     * A SyncTarget that can be added to a sync set.
-     */
-    public interface SyncTarget {
-        /**
-         * Called when the SyncTarget has been added to a SyncGroup as is ready to begin handing a
-         * sync request. When invoked, the implementor is required to call
-         * {@link TransactionReadyCallback#onTransactionReady(Transaction)} in order for this
-         * SurfaceSyncGroup to fully complete.
-         *
-         * Always invoked on the thread that initiated the call to {@link #addToSync(SyncTarget)}
-         *
-         * @param parentSyncGroup The sync group this target has been added to.
-         * @param transactionReadyCallback A TransactionReadyCallback that the caller must invoke
-         *                                 onTransactionReady
-         */
-        void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
-                TransactionReadyCallback transactionReadyCallback);
-    }
-
     /**
      * Interface so the SurfaceSyncer can know when it's safe to start and when everything has been
      * completed. The caller should invoke the calls when the rendering has started and finished a
      * frame.
      */
-    public interface TransactionReadyCallback {
+    private interface TransactionReadyCallback {
         /**
          * Invoked when the transaction is ready to sync.
          *
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 9f23f24..b9ca557 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -252,11 +252,13 @@
      * @param sharedMemory The unrestricted data blob to provide to the
      * {@link HotwordDetectionService}. Use this to provide the hotword models data or other
      * such data to the trusted process.
+     * @param token Use this to identify which detector calls this method.
      */
     @EnforcePermission("MANAGE_HOTWORD_DETECTION")
     void updateState(
             in PersistableBundle options,
-            in SharedMemory sharedMemory);
+            in SharedMemory sharedMemory,
+            in IBinder token);
 
     /**
      * Set configuration and pass read-only data to hotword detection service when creating
@@ -272,6 +274,7 @@
      * @param sharedMemory The unrestricted data blob to provide to the
      * {@link HotwordDetectionService}. Use this to provide the hotword models data or other
      * such data to the trusted process.
+     * @param token Use this to identify which detector calls this method.
      * @param callback Use this to report {@link HotwordDetectionService} status.
      * @param detectorType Indicate which detector is used.
      */
@@ -280,10 +283,18 @@
             in Identity originatorIdentity,
             in PersistableBundle options,
             in SharedMemory sharedMemory,
+            in IBinder token,
             in IHotwordRecognitionStatusCallback callback,
             int detectorType);
 
     /**
+     * Destroy the detector callback.
+     *
+     * @param token Indicate which callback will be destroyed.
+     */
+    void destroyDetector(in IBinder token);
+
+    /**
      * Requests to shutdown hotword detection service.
      */
     void shutdownHotwordDetectionService();
@@ -298,6 +309,7 @@
         in ParcelFileDescriptor audioStream,
         in AudioFormat audioFormat,
         in PersistableBundle options,
+        in IBinder token,
         in IMicrophoneHotwordDetectionVoiceInteractionCallback callback);
 
     /**
diff --git a/core/java/com/android/internal/app/IVoiceInteractionSessionListener.aidl b/core/java/com/android/internal/app/IVoiceInteractionSessionListener.aidl
index 6e40988..46f78e2 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionSessionListener.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionSessionListener.aidl
@@ -31,6 +31,8 @@
 
     /**
      * Called when a voice session window is shown/hidden.
+     * Caution that there could be duplicated visibility change callbacks, it's up to the listener
+     * to dedup those events.
      */
     void onVoiceSessionWindowVisibilityChanged(boolean visible);
 
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 75f0bf5..7bd6ec8 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -780,17 +780,17 @@
         // 2. The returned string should be the same with the name defined in atoms.proto.
         switch (cujType) {
             case CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE:
-                return "SHADE_EXPAND_COLLAPSE";
+                return "NOTIFICATION_SHADE_EXPAND_COLLAPSE";
             case CUJ_NOTIFICATION_SHADE_SCROLL_FLING:
-                return "SHADE_SCROLL_FLING";
+                return "NOTIFICATION_SHADE_SCROLL_FLING";
             case CUJ_NOTIFICATION_SHADE_ROW_EXPAND:
-                return "SHADE_ROW_EXPAND";
+                return "NOTIFICATION_SHADE_ROW_EXPAND";
             case CUJ_NOTIFICATION_SHADE_ROW_SWIPE:
-                return "SHADE_ROW_SWIPE";
+                return "NOTIFICATION_SHADE_ROW_SWIPE";
             case CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE:
-                return "SHADE_QS_EXPAND_COLLAPSE";
+                return "NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE";
             case CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE:
-                return "SHADE_QS_SCROLL_SWIPE";
+                return "NOTIFICATION_SHADE_QS_SCROLL_SWIPE";
             case CUJ_LAUNCHER_APP_LAUNCH_FROM_RECENTS:
                 return "LAUNCHER_APP_LAUNCH_FROM_RECENTS";
             case CUJ_LAUNCHER_APP_LAUNCH_FROM_ICON:
@@ -880,9 +880,9 @@
             case CUJ_SPLIT_SCREEN_EXIT:
                 return "SPLIT_SCREEN_EXIT";
             case CUJ_LOCKSCREEN_LAUNCH_CAMERA:
-                return "CUJ_LOCKSCREEN_LAUNCH_CAMERA";
+                return "LOCKSCREEN_LAUNCH_CAMERA";
             case CUJ_SPLIT_SCREEN_RESIZE:
-                return "CUJ_SPLIT_SCREEN_RESIZE";
+                return "SPLIT_SCREEN_RESIZE";
             case CUJ_SETTINGS_SLIDER:
                 return "SETTINGS_SLIDER";
             case CUJ_TAKE_SCREENSHOT:
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index c3361a2..66d64c4 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -49,6 +49,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.ConcurrentModificationException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -163,10 +164,6 @@
      */
     private int mCurrentParcelEnd;
     /**
-     * When iterating history files, the current record count.
-     */
-    private int mRecordCount = 0;
-    /**
      * Used when BatteryStatsImpl object is created from deserialization of a parcel,
      * such as Settings app or checkin file, to iterate over history parcels.
      */
@@ -199,10 +196,8 @@
     private boolean mMeasuredEnergyHeaderWritten = false;
     private boolean mCpuUsageHeaderWritten = false;
     private final VarintParceler mVarintParceler = new VarintParceler();
-
     private byte mLastHistoryStepLevel = 0;
-
-    private BatteryStatsHistoryIterator mBatteryStatsHistoryIterator;
+    private boolean mMutable = true;
 
     /**
      * A delegate responsible for computing additional details for a step in battery history.
@@ -493,25 +488,21 @@
      * @return always return true.
      */
     public BatteryStatsHistoryIterator iterate() {
-        mRecordCount = 0;
         mCurrentFileIndex = 0;
         mCurrentParcel = null;
         mCurrentParcelEnd = 0;
         mParcelIndex = 0;
-        mBatteryStatsHistoryIterator = new BatteryStatsHistoryIterator(this);
-        return mBatteryStatsHistoryIterator;
+        mMutable = false;
+        return new BatteryStatsHistoryIterator(this);
     }
 
     /**
      * Finish iterating history files and history buffer.
      */
-    void finishIteratingHistory() {
+    void iteratorFinished() {
         // setDataPosition so mHistoryBuffer Parcel can be written.
         mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
-        mBatteryStatsHistoryIterator = null;
-        if (DEBUG) {
-            Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
-        }
+        mMutable = true;
     }
 
     /**
@@ -519,17 +510,11 @@
      * history file, when reached the mActiveFile (highest numbered history file), do not read from
      * mActiveFile, read from history buffer instead because the buffer has more updated data.
      *
-     * @param out a history item.
      * @return The parcel that has next record. null if finished all history files and history
      * buffer
      */
-    public Parcel getNextParcel(HistoryItem out) {
-        if (mRecordCount == 0) {
-            // reset out if it is the first record.
-            out.clear();
-        }
-        ++mRecordCount;
-
+    @Nullable
+    public Parcel getNextParcel() {
         // First iterate through all records in current parcel.
         if (mCurrentParcel != null) {
             if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
@@ -1270,6 +1255,10 @@
             return;
         }
 
+        if (!mMutable) {
+            throw new ConcurrentModificationException("Battery history is not writable");
+        }
+
         final long timeDiffMs = (mHistoryBaseTimeMs + elapsedRealtimeMs) - mHistoryLastWritten.time;
         final int diffStates = mHistoryLastWritten.states ^ (cur.states & mActiveHistoryStates);
         final int diffStates2 = mHistoryLastWritten.states2 ^ (cur.states2 & mActiveHistoryStates2);
@@ -1390,8 +1379,8 @@
 
     private void writeHistoryItem(long elapsedRealtimeMs,
             @SuppressWarnings("UnusedVariable") long uptimeMs, HistoryItem cur, byte cmd) {
-        if (mBatteryStatsHistoryIterator != null) {
-            throw new IllegalStateException("Can't do this while iterating history!");
+        if (!mMutable) {
+            throw new ConcurrentModificationException("Battery history is not writable");
         }
         mHistoryBufferLastPos = mHistoryBuffer.dataPosition();
         mHistoryLastLastWritten.setTo(mHistoryLastWritten);
@@ -1687,7 +1676,10 @@
                     Slog.i(TAG, "WRITE DELTA: cpuUsageDetails=" + cur.cpuUsageDetails);
                 }
                 if (!mCpuUsageHeaderWritten) {
-                    dest.writeStringArray(cur.cpuUsageDetails.cpuBracketDescriptions);
+                    dest.writeInt(cur.cpuUsageDetails.cpuBracketDescriptions.length);
+                    for (String desc: cur.cpuUsageDetails.cpuBracketDescriptions) {
+                        dest.writeString(desc);
+                    }
                     mCpuUsageHeaderWritten = true;
                 }
                 dest.writeInt(cur.cpuUsageDetails.uid);
diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
index 09fe100..b88116d 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -23,10 +23,13 @@
 import android.util.Slog;
 import android.util.SparseArray;
 
+import java.util.Iterator;
+
 /**
  * An iterator for {@link BatteryStats.HistoryItem}'s.
  */
-public class BatteryStatsHistoryIterator {
+public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.HistoryItem>,
+        AutoCloseable {
     private static final boolean DEBUG = false;
     private static final String TAG = "BatteryStatsHistoryItr";
     private final BatteryStatsHistory mBatteryStatsHistory;
@@ -38,29 +41,51 @@
     private final BatteryStatsHistory.VarintParceler mVarintParceler =
             new BatteryStatsHistory.VarintParceler();
 
+    private final BatteryStats.HistoryItem mHistoryItem = new BatteryStats.HistoryItem();
+
+    private static final int MAX_ENERGY_CONSUMER_COUNT = 100;
+    private static final int MAX_CPU_BRACKET_COUNT = 100;
+
     public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history) {
         mBatteryStatsHistory = history;
+        mHistoryItem.clear();
+    }
+
+    @Override
+    public boolean hasNext() {
+        Parcel p = mBatteryStatsHistory.getNextParcel();
+        if (p == null) {
+            close();
+            return false;
+        }
+        return true;
     }
 
     /**
-     * Retrieves the next HistoryItem from battery history, if available. Returns false if there
+     * Retrieves the next HistoryItem from battery history, if available. Returns null if there
      * are no more items.
      */
-    public boolean next(BatteryStats.HistoryItem out) {
-        Parcel p = mBatteryStatsHistory.getNextParcel(out);
+    @Override
+    public BatteryStats.HistoryItem next() {
+        Parcel p = mBatteryStatsHistory.getNextParcel();
         if (p == null) {
-            mBatteryStatsHistory.finishIteratingHistory();
-            return false;
+            close();
+            return null;
         }
 
-        final long lastRealtimeMs = out.time;
-        final long lastWalltimeMs = out.currentTime;
-        readHistoryDelta(p, out);
-        if (out.cmd != BatteryStats.HistoryItem.CMD_CURRENT_TIME
-                && out.cmd != BatteryStats.HistoryItem.CMD_RESET && lastWalltimeMs != 0) {
-            out.currentTime = lastWalltimeMs + (out.time - lastRealtimeMs);
+        final long lastRealtimeMs = mHistoryItem.time;
+        final long lastWalltimeMs = mHistoryItem.currentTime;
+        try {
+            readHistoryDelta(p, mHistoryItem);
+        } catch (Throwable t) {
+            Slog.wtf(TAG, "Corrupted battery history", t);
+            return null;
         }
-        return true;
+        if (mHistoryItem.cmd != BatteryStats.HistoryItem.CMD_CURRENT_TIME
+                && mHistoryItem.cmd != BatteryStats.HistoryItem.CMD_RESET && lastWalltimeMs != 0) {
+            mHistoryItem.currentTime = lastWalltimeMs + (mHistoryItem.time - lastRealtimeMs);
+        }
+        return mHistoryItem;
     }
 
     private void readHistoryDelta(Parcel src, BatteryStats.HistoryItem cur) {
@@ -210,6 +235,12 @@
                 }
 
                 final int consumerCount = src.readInt();
+                if (consumerCount > MAX_ENERGY_CONSUMER_COUNT) {
+                    // Check to avoid a heap explosion in case the parcel is corrupted
+                    throw new IllegalStateException(
+                            "EnergyConsumer count too high: " + consumerCount
+                                    + ". Max = " + MAX_ENERGY_CONSUMER_COUNT);
+                }
                 mMeasuredEnergyDetails.consumers =
                         new BatteryStats.MeasuredEnergyDetails.EnergyConsumer[consumerCount];
                 mMeasuredEnergyDetails.chargeUC = new long[consumerCount];
@@ -236,7 +267,16 @@
 
             if ((extensionFlags & BatteryStatsHistory.EXTENSION_CPU_USAGE_HEADER_FLAG) != 0) {
                 mCpuUsageDetails = new BatteryStats.CpuUsageDetails();
-                mCpuUsageDetails.cpuBracketDescriptions = src.readStringArray();
+                final int cpuBracketCount = src.readInt();
+                if (cpuBracketCount > MAX_CPU_BRACKET_COUNT) {
+                    // Check to avoid a heap explosion in case the parcel is corrupted
+                    throw new IllegalStateException("Too many CPU brackets: " + cpuBracketCount
+                            + ". Max = " + MAX_CPU_BRACKET_COUNT);
+                }
+                mCpuUsageDetails.cpuBracketDescriptions = new String[cpuBracketCount];
+                for (int i = 0; i < cpuBracketCount; i++) {
+                    mCpuUsageDetails.cpuBracketDescriptions[i] = src.readString();
+                }
                 mCpuUsageDetails.cpuUsageMs =
                         new long[mCpuUsageDetails.cpuBracketDescriptions.length];
             } else if (mCpuUsageDetails != null) {
@@ -294,7 +334,8 @@
     /**
      * Should be called when iteration is complete.
      */
+    @Override
     public void close() {
-        mBatteryStatsHistory.finishIteratingHistory();
+        mBatteryStatsHistory.iteratorFinished();
     }
 }
diff --git a/core/java/com/android/internal/security/TEST_MAPPING b/core/java/com/android/internal/security/TEST_MAPPING
index 803760c..b47ecec 100644
--- a/core/java/com/android/internal/security/TEST_MAPPING
+++ b/core/java/com/android/internal/security/TEST_MAPPING
@@ -14,6 +14,19 @@
     {
       "name": "ApkVerityTest",
       "file_patterns": ["VerityUtils\\.java"]
+    },
+    {
+      "name": "UpdatableSystemFontTest",
+      "file_patterns": ["VerityUtils\\.java"]
+    },
+    {
+      "name": "CtsAppSecurityHostTestCases",
+      "options": [
+        {
+          "include-filter": "android.appsecurity.cts.ApkVerityInstallTest"
+        }
+      ],
+      "file_patterns": ["VerityUtils\\.java"]
     }
   ]
 }
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 3ab11a8..786941f 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -17,7 +17,6 @@
 package com.android.internal.security;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.os.Build;
 import android.os.SystemProperties;
 import android.system.Os;
@@ -41,9 +40,6 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
@@ -58,9 +54,6 @@
      */
     public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig";
 
-    /** The maximum size of signature file.  This is just to avoid potential abuse. */
-    private static final int MAX_SIGNATURE_FILE_SIZE_BYTES = 8192;
-
     /** SHA256 hash size. */
     private static final int HASH_SIZE_BYTES = 32;
 
@@ -79,26 +72,9 @@
         return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION;
     }
 
-    /** Enables fs-verity for the file with an optional PKCS#7 detached signature file. */
-    public static void setUpFsverity(@NonNull String filePath, @Nullable String signaturePath)
-            throws IOException {
-        byte[] rawSignature = null;
-        if (signaturePath != null) {
-            Path path = Paths.get(signaturePath);
-            if (Files.size(path) > MAX_SIGNATURE_FILE_SIZE_BYTES) {
-                throw new SecurityException("Signature file is unexpectedly large: "
-                        + signaturePath);
-            }
-            rawSignature = Files.readAllBytes(path);
-        }
-        setUpFsverity(filePath, rawSignature);
-    }
-
-    /** Enables fs-verity for the file with an optional PKCS#7 detached signature bytes. */
-    public static void setUpFsverity(@NonNull String filePath, @Nullable byte[] pkcs7Signature)
-            throws IOException {
-        // This will fail if the public key is not already in .fs-verity kernel keyring.
-        int errno = enableFsverityNative(filePath, pkcs7Signature);
+    /** Enables fs-verity for the file without signature. */
+    public static void setUpFsverity(@NonNull String filePath) throws IOException {
+        int errno = enableFsverityNative(filePath);
         if (errno != 0) {
             throw new IOException("Failed to enable fs-verity on " + filePath + ": "
                     + Os.strerror(errno));
@@ -234,8 +210,7 @@
         return buffer.array();
     }
 
-    private static native int enableFsverityNative(@NonNull String filePath,
-            @Nullable byte[] pkcs7Signature);
+    private static native int enableFsverityNative(@NonNull String filePath);
     private static native int measureFsverityNative(@NonNull String filePath,
             @NonNull byte[] digest);
     private static native int statxForFsverityNative(@NonNull String filePath);
diff --git a/core/java/com/android/internal/telephony/ICarrierConfigChangeListener.aidl b/core/java/com/android/internal/telephony/ICarrierConfigChangeListener.aidl
new file mode 100644
index 0000000..0f7ab0a
--- /dev/null
+++ b/core/java/com/android/internal/telephony/ICarrierConfigChangeListener.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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.telephony;
+
+oneway interface ICarrierConfigChangeListener {
+    void onCarrierConfigChanged(int slotIndex, int subId, int carrierId, int specificCarrierId);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index 7ba2686..54936c6 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -32,6 +32,7 @@
 import android.telephony.ServiceState;
 import android.telephony.SignalStrength;
 import android.telephony.emergency.EmergencyNumber;
+import com.android.internal.telephony.ICarrierConfigChangeListener;
 import com.android.internal.telephony.ICarrierPrivilegesCallback;
 import com.android.internal.telephony.IPhoneStateListener;
 import com.android.internal.telephony.IOnSubscriptionsChangedListener;
@@ -109,4 +110,8 @@
             int phoneId, in List<String> privilegedPackageNames, in int[] privilegedUids);
     void notifyCarrierServiceChanged(int phoneId, in String packageName, int uid);
 
+    void addCarrierConfigChangeListener(ICarrierConfigChangeListener listener,
+            String pkg, String featureId);
+    void removeCarrierConfigChangeListener(ICarrierConfigChangeListener listener, String pkg);
+    void notifyCarrierConfigChanged(int phoneId, int subId, int carrierId, int specificCarrierId);
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 953b36b..65f5522 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1508,7 +1508,8 @@
                         STRONG_AUTH_REQUIRED_AFTER_LOCKOUT,
                         STRONG_AUTH_REQUIRED_AFTER_TIMEOUT,
                         STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
-                        STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT})
+                        STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT,
+                        SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED})
         @Retention(RetentionPolicy.SOURCE)
         public @interface StrongAuthFlags {}
 
@@ -1561,11 +1562,18 @@
         public static final int STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT = 0x80;
 
         /**
+         * Some authentication is required because the trustagent either timed out or was disabled
+         * manually.
+         */
+        public static final int SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED = 0x100;
+
+        /**
          * Strong auth flags that do not prevent biometric methods from being accepted as auth.
          * If any other flags are set, biometric authentication is disabled.
          */
         private static final int ALLOWING_BIOMETRIC = STRONG_AUTH_NOT_REQUIRED
-                | SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
+                | SOME_AUTH_REQUIRED_AFTER_USER_REQUEST
+                | SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
 
         private final SparseIntArray mStrongAuthRequiredForUser = new SparseIntArray();
         private final H mHandler;
@@ -1742,7 +1750,7 @@
     }
 
     public static boolean userOwnsFrpCredential(Context context, UserInfo info) {
-        return info != null && info.isPrimary() && info.isAdmin() && frpCredentialEnabled(context);
+        return info != null && info.isMain() && info.isAdmin() && frpCredentialEnabled(context);
     }
 
     public static boolean frpCredentialEnabled(Context context) {
diff --git a/core/jni/android_media_AudioFormat.h b/core/jni/android_media_AudioFormat.h
index 962b501..a9b1906 100644
--- a/core/jni/android_media_AudioFormat.h
+++ b/core/jni/android_media_AudioFormat.h
@@ -49,6 +49,7 @@
 #define ENCODING_DRA 28
 #define ENCODING_DTS_HD_MA 29
 #define ENCODING_DTS_UHD_P2 30
+#define ENCODING_DSD 31
 
 #define ENCODING_INVALID    0
 #define ENCODING_DEFAULT    1
@@ -122,6 +123,8 @@
         return AUDIO_FORMAT_DTS_HD_MA;
     case ENCODING_DTS_UHD_P2:
         return AUDIO_FORMAT_DTS_UHD_P2;
+    case ENCODING_DSD:
+        return AUDIO_FORMAT_DSD;
     default:
         return AUDIO_FORMAT_INVALID;
     }
@@ -201,6 +204,8 @@
         return ENCODING_DTS_UHD_P2;
     case AUDIO_FORMAT_DEFAULT:
         return ENCODING_DEFAULT;
+    case AUDIO_FORMAT_DSD:
+        return ENCODING_DSD;
     default:
         return ENCODING_INVALID;
     }
diff --git a/core/jni/android_media_AudioProfile.h b/core/jni/android_media_AudioProfile.h
index 446bd64..5096250e 100644
--- a/core/jni/android_media_AudioProfile.h
+++ b/core/jni/android_media_AudioProfile.h
@@ -25,6 +25,7 @@
 // keep these values in sync with AudioProfile.java
 #define ENCAPSULATION_TYPE_NONE 0
 #define ENCAPSULATION_TYPE_IEC61937 1
+#define ENCAPSULATION_TYPE_PCM 2
 
 static inline status_t audioEncapsulationTypeFromNative(
         audio_encapsulation_type_t nEncapsulationType, int* encapsulationType) {
@@ -36,6 +37,9 @@
         case AUDIO_ENCAPSULATION_TYPE_IEC61937:
             *encapsulationType = ENCAPSULATION_TYPE_IEC61937;
             break;
+        case AUDIO_ENCAPSULATION_TYPE_PCM:
+            *encapsulationType = ENCAPSULATION_TYPE_PCM;
+            break;
         default:
             result = BAD_VALUE;
     }
diff --git a/core/jni/com_android_internal_security_VerityUtils.cpp b/core/jni/com_android_internal_security_VerityUtils.cpp
index dabee69..3e5689b 100644
--- a/core/jni/com_android_internal_security_VerityUtils.cpp
+++ b/core/jni/com_android_internal_security_VerityUtils.cpp
@@ -23,7 +23,6 @@
 #include <linux/fsverity.h>
 #include <linux/stat.h>
 #include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedPrimitiveArray.h>
 #include <nativehelper/ScopedUtfChars.h>
 #include <string.h>
 #include <sys/ioctl.h>
@@ -39,7 +38,7 @@
 
 namespace {
 
-int enableFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath, jbyteArray signature) {
+int enableFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath) {
     ScopedUtfChars path(env, filePath);
     if (path.c_str() == nullptr) {
         return EINVAL;
@@ -56,18 +55,6 @@
     arg.salt_size = 0;
     arg.salt_ptr = reinterpret_cast<uintptr_t>(nullptr);
 
-    if (signature != nullptr) {
-        ScopedByteArrayRO signature_bytes(env, signature);
-        if (signature_bytes.get() == nullptr) {
-            return EINVAL;
-        }
-        arg.sig_size = signature_bytes.size();
-        arg.sig_ptr = reinterpret_cast<uintptr_t>(signature_bytes.get());
-    } else {
-        arg.sig_size = 0;
-        arg.sig_ptr = reinterpret_cast<uintptr_t>(nullptr);
-    }
-
     if (ioctl(rfd.get(), FS_IOC_ENABLE_VERITY, &arg) < 0) {
         return errno;
     }
@@ -138,7 +125,7 @@
     return 0;
 }
 const JNINativeMethod sMethods[] = {
-        {"enableFsverityNative", "(Ljava/lang/String;[B)I", (void *)enableFsverity},
+        {"enableFsverityNative", "(Ljava/lang/String;)I", (void *)enableFsverity},
         {"statxForFsverityNative", "(Ljava/lang/String;)I", (void *)statxForFsverity},
         {"measureFsverityNative", "(Ljava/lang/String;[B)I", (void *)measureFsverity},
 };
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index 9070933..db391f7 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -16,7 +16,8 @@
 per-file package_item_info.proto = toddke@google.com,patb@google.com
 per-file usagestatsservice.proto, usagestatsservice_v2.proto = file:/core/java/android/app/usage/OWNERS
 per-file apphibernationservice.proto = file:/core/java/android/apphibernation/OWNERS
-per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com,ewol@google.com
+per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com
+per-file background_install_control.proto = wenhaowang@google.com,georgechan@google.com,billylau@google.com
 
 # Biometrics
 jaggies@google.com
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 556636dd..e6f942e 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -596,6 +596,15 @@
     optional SettingProto theme_customization_overlay_packages = 75 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto trust_agents_initialized = 57 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
+    message TrackpadGesture {
+        optional SettingProto trackpad_gesture_back_enabled = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto trackpad_gesture_home_enabled = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto trackpad_gesture_overview_enabled = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto trackpad_gesture_notification_enabled = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto trackpad_gesture_quick_switch_enabled = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    }
+    optional TrackpadGesture trackpad_gesture = 94;
+
     message Tts {
         option (android.msg_privacy).dest = DEST_EXPLICIT;
 
@@ -687,5 +696,5 @@
 
     // Please insert fields in alphabetical order and group them into messages
     // if possible (to avoid reaching the method limit).
-    // Next tag = 94;
+    // Next tag = 95;
 }
diff --git a/core/proto/android/server/background_install_control.proto b/core/proto/android/server/background_install_control.proto
new file mode 100644
index 0000000..38e6b4d
--- /dev/null
+++ b/core/proto/android/server/background_install_control.proto
@@ -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.
+ */
+
+syntax = "proto2";
+package com.android.server.pm;
+
+option java_multiple_files = true;
+
+// Proto for the background installed packages.
+// It's used for serializing the background installed package info to disk.
+message BackgroundInstalledPackagesProto {
+  repeated BackgroundInstalledPackageProto bg_installed_pkg = 1;
+}
+
+// Proto for the background installed package entry
+message BackgroundInstalledPackageProto {
+  optional string package_name = 1;
+  optional int32 user_id = 2;
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5853686..fd4d4f8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4937,7 +4937,7 @@
     <!-- Allows an application to manage the companion devices.
          @hide -->
     <permission android:name="android.permission.MANAGE_COMPANION_DEVICES"
-                android:protectionLevel="signature|role" />
+                android:protectionLevel="module|signature|role" />
 
     <!-- Allows an application to subscribe to notifications about the presence status change
          of their associated companion device
@@ -4975,6 +4975,15 @@
     <permission android:name="android.permission.ROTATE_SURFACE_FLINGER"
         android:protectionLevel="signature|recents" />
 
+    <!-- @SystemApi Allows an application to provide hints to SurfaceFlinger that can influence
+         its wakes up time to compose the next frame. This is a subset of the capabilities granted
+         by {@link #ACCESS_SURFACE_FLINGER}.
+         <p>Not for use by third-party applications.
+         @hide
+    -->
+    <permission android:name="android.permission.WAKEUP_SURFACE_FLINGER"
+        android:protectionLevel="signature|recents" />
+
     <!-- Allows an application to take screen shots and more generally
          get access to the frame buffer data.
          <p>Not for use by third-party applications.
diff --git a/core/res/OWNERS b/core/res/OWNERS
index a2ef400..b878189 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -21,6 +21,11 @@
 tsuji@google.com
 yamasani@google.com
 
+# WindowManager team
+# TODO(262451702): Move WindowManager configs out of config.xml in a separate file
+per-file core/res/res/values/config.xml = file:/services/core/java/com/android/server/wm/OWNERS
+per-file core/res/res/values/symbols.xml = file:/services/core/java/com/android/server/wm/OWNERS
+
 # Resources finalization
 per-file res/xml/public-staging.xml = file:/tools/aapt2/OWNERS
 per-file res/xml/public-final.xml = file:/tools/aapt2/OWNERS
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 2d832bc..c8b0601 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -8987,6 +8987,10 @@
         <!-- The service that provides {@link android.service.voice.HotwordDetectionService}.
              @hide @SystemApi -->
         <attr name="hotwordDetectionService" format="string" />
+        <!-- The service that provides {@link android.service.voice.VisualQueryDetectionService}.
+             @hide @SystemApi -->
+        <attr name="visualQueryDetectionService" format="string" />
+
     </declare-styleable>
 
     <!-- Use <code>game-service</code> as the root tag of the XML resource that
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2fb766e..f995a6e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5281,6 +5281,10 @@
     <!-- Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. -->
     <bool name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled">false</bool>
 
+    <!-- Whether the specific behaviour for translucent activities letterboxing is enabled.
+         TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
+    <bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
+
     <!-- Whether a camera compat controller is enabled to allow the user to apply or revert
          treatment for stretched issues in camera viewfinder. -->
     <bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool>
diff --git a/core/res/res/values/locale_config.xml b/core/res/res/values/locale_config.xml
index 78ec145..bd93aa9 100644
--- a/core/res/res/values/locale_config.xml
+++ b/core/res/res/values/locale_config.xml
@@ -23,7 +23,7 @@
         <item>ak-GH</item> <!-- Akan (Ghana) -->
         <item>am-ET</item> <!-- Amharic (Ethiopia) -->
         <item>ar-AE</item> <!-- Arabic (United Arab Emirates) -->
-        <item>ar-AE-u-nu-arab</item> <!-- Arabic (United Arab Emirates, Arabic Digits) -->
+        <item>ar-AE-u-nu-arab</item> <!-- Arabic (United Arab Emirates, Arabic-Indic Digits) -->
         <item>ar-BH</item> <!-- Arabic (Bahrain) -->
         <item>ar-BH-u-nu-latn</item> <!-- Arabic (Bahrain, Western Digits) -->
         <item>ar-DJ</item> <!-- Arabic (Djibouti) -->
@@ -108,6 +108,7 @@
         <item>cgg-UG</item> <!-- Chiga (Uganda) -->
         <item>chr-US</item> <!-- Cherokee (United States) -->
         <item>cs-CZ</item> <!-- Czech (Czechia) -->
+        <item>cv-RU</item> <!-- Chuvash (Russia) -->
         <item>cy-GB</item> <!-- Welsh (United Kingdom) -->
         <item>da-DK</item> <!-- Danish (Denmark) -->
         <item>da-GL</item> <!-- Danish (Greenland) -->
@@ -270,42 +271,42 @@
         <item>fa-AF-u-nu-latn</item> <!-- Persian (Afghanistan, Western Digits) -->
         <item>fa-IR</item> <!-- Persian (Iran) -->
         <item>fa-IR-u-nu-latn</item> <!-- Persian (Iran, Western Digits) -->
-        <item>ff-Adlm-BF</item> <!-- Fulah (Adlam, Burkina Faso) -->
-        <item>ff-Adlm-BF-u-nu-latn</item> <!-- Fulah (Adlam, Burkina Faso, Western Digits) -->
-        <item>ff-Adlm-CM</item> <!-- Fulah (Adlam, Cameroon) -->
-        <item>ff-Adlm-CM-u-nu-latn</item> <!-- Fulah (Adlam, Cameroon, Western Digits) -->
-        <item>ff-Adlm-GH</item> <!-- Fulah (Adlam, Ghana) -->
-        <item>ff-Adlm-GH-u-nu-latn</item> <!-- Fulah (Adlam, Ghana, Western Digits) -->
-        <item>ff-Adlm-GM</item> <!-- Fulah (Adlam, Gambia) -->
-        <item>ff-Adlm-GM-u-nu-latn</item> <!-- Fulah (Adlam, Gambia, Western Digits) -->
-        <item>ff-Adlm-GN</item> <!-- Fulah (Adlam, Guinea) -->
-        <item>ff-Adlm-GN-u-nu-latn</item> <!-- Fulah (Adlam, Guinea, Western Digits) -->
-        <item>ff-Adlm-GW</item> <!-- Fulah (Adlam, Guinea-Bissau) -->
-        <item>ff-Adlm-GW-u-nu-latn</item> <!-- Fulah (Adlam, Guinea-Bissau, Western Digits) -->
-        <item>ff-Adlm-LR</item> <!-- Fulah (Adlam, Liberia) -->
-        <item>ff-Adlm-LR-u-nu-latn</item> <!-- Fulah (Adlam, Liberia, Western Digits) -->
-        <item>ff-Adlm-MR</item> <!-- Fulah (Adlam, Mauritania) -->
-        <item>ff-Adlm-MR-u-nu-latn</item> <!-- Fulah (Adlam, Mauritania, Western Digits) -->
-        <item>ff-Adlm-NE</item> <!-- Fulah (Adlam, Niger) -->
-        <item>ff-Adlm-NE-u-nu-latn</item> <!-- Fulah (Adlam, Niger, Western Digits) -->
-        <item>ff-Adlm-NG</item> <!-- Fulah (Adlam, Nigeria) -->
-        <item>ff-Adlm-NG-u-nu-latn</item> <!-- Fulah (Adlam, Nigeria, Western Digits) -->
-        <item>ff-Adlm-SL</item> <!-- Fulah (Adlam, Sierra Leone) -->
-        <item>ff-Adlm-SL-u-nu-latn</item> <!-- Fulah (Adlam, Sierra Leone, Western Digits) -->
-        <item>ff-Adlm-SN</item> <!-- Fulah (Adlam, Senegal) -->
-        <item>ff-Adlm-SN-u-nu-latn</item> <!-- Fulah (Adlam, Senegal, Western Digits) -->
-        <item>ff-Latn-BF</item> <!-- Fulah (Latin, Burkina Faso) -->
-        <item>ff-Latn-CM</item> <!-- Fulah (Latin, Cameroon) -->
-        <item>ff-Latn-GH</item> <!-- Fulah (Latin, Ghana) -->
-        <item>ff-Latn-GM</item> <!-- Fulah (Latin, Gambia) -->
-        <item>ff-Latn-GN</item> <!-- Fulah (Latin, Guinea) -->
-        <item>ff-Latn-GW</item> <!-- Fulah (Latin, Guinea-Bissau) -->
-        <item>ff-Latn-LR</item> <!-- Fulah (Latin, Liberia) -->
-        <item>ff-Latn-MR</item> <!-- Fulah (Latin, Mauritania) -->
-        <item>ff-Latn-NE</item> <!-- Fulah (Latin, Niger) -->
-        <item>ff-Latn-NG</item> <!-- Fulah (Latin, Nigeria) -->
-        <item>ff-Latn-SL</item> <!-- Fulah (Latin, Sierra Leone) -->
-        <item>ff-Latn-SN</item> <!-- Fulah (Latin, Senegal) -->
+        <item>ff-Adlm-BF</item> <!-- Fula (Adlam, Burkina Faso) -->
+        <item>ff-Adlm-BF-u-nu-latn</item> <!-- Fula (Adlam, Burkina Faso, Western Digits) -->
+        <item>ff-Adlm-CM</item> <!-- Fula (Adlam, Cameroon) -->
+        <item>ff-Adlm-CM-u-nu-latn</item> <!-- Fula (Adlam, Cameroon, Western Digits) -->
+        <item>ff-Adlm-GH</item> <!-- Fula (Adlam, Ghana) -->
+        <item>ff-Adlm-GH-u-nu-latn</item> <!-- Fula (Adlam, Ghana, Western Digits) -->
+        <item>ff-Adlm-GM</item> <!-- Fula (Adlam, Gambia) -->
+        <item>ff-Adlm-GM-u-nu-latn</item> <!-- Fula (Adlam, Gambia, Western Digits) -->
+        <item>ff-Adlm-GN</item> <!-- Fula (Adlam, Guinea) -->
+        <item>ff-Adlm-GN-u-nu-latn</item> <!-- Fula (Adlam, Guinea, Western Digits) -->
+        <item>ff-Adlm-GW</item> <!-- Fula (Adlam, Guinea-Bissau) -->
+        <item>ff-Adlm-GW-u-nu-latn</item> <!-- Fula (Adlam, Guinea-Bissau, Western Digits) -->
+        <item>ff-Adlm-LR</item> <!-- Fula (Adlam, Liberia) -->
+        <item>ff-Adlm-LR-u-nu-latn</item> <!-- Fula (Adlam, Liberia, Western Digits) -->
+        <item>ff-Adlm-MR</item> <!-- Fula (Adlam, Mauritania) -->
+        <item>ff-Adlm-MR-u-nu-latn</item> <!-- Fula (Adlam, Mauritania, Western Digits) -->
+        <item>ff-Adlm-NE</item> <!-- Fula (Adlam, Niger) -->
+        <item>ff-Adlm-NE-u-nu-latn</item> <!-- Fula (Adlam, Niger, Western Digits) -->
+        <item>ff-Adlm-NG</item> <!-- Fula (Adlam, Nigeria) -->
+        <item>ff-Adlm-NG-u-nu-latn</item> <!-- Fula (Adlam, Nigeria, Western Digits) -->
+        <item>ff-Adlm-SL</item> <!-- Fula (Adlam, Sierra Leone) -->
+        <item>ff-Adlm-SL-u-nu-latn</item> <!-- Fula (Adlam, Sierra Leone, Western Digits) -->
+        <item>ff-Adlm-SN</item> <!-- Fula (Adlam, Senegal) -->
+        <item>ff-Adlm-SN-u-nu-latn</item> <!-- Fula (Adlam, Senegal, Western Digits) -->
+        <item>ff-Latn-BF</item> <!-- Fula (Latin, Burkina Faso) -->
+        <item>ff-Latn-CM</item> <!-- Fula (Latin, Cameroon) -->
+        <item>ff-Latn-GH</item> <!-- Fula (Latin, Ghana) -->
+        <item>ff-Latn-GM</item> <!-- Fula (Latin, Gambia) -->
+        <item>ff-Latn-GN</item> <!-- Fula (Latin, Guinea) -->
+        <item>ff-Latn-GW</item> <!-- Fula (Latin, Guinea-Bissau) -->
+        <item>ff-Latn-LR</item> <!-- Fula (Latin, Liberia) -->
+        <item>ff-Latn-MR</item> <!-- Fula (Latin, Mauritania) -->
+        <item>ff-Latn-NE</item> <!-- Fula (Latin, Niger) -->
+        <item>ff-Latn-NG</item> <!-- Fula (Latin, Nigeria) -->
+        <item>ff-Latn-SL</item> <!-- Fula (Latin, Sierra Leone) -->
+        <item>ff-Latn-SN</item> <!-- Fula (Latin, Senegal) -->
         <item>fi-FI</item> <!-- Finnish (Finland) -->
         <item>fil-PH</item> <!-- Filipino (Philippines) -->
         <item>fo-DK</item> <!-- Faroese (Denmark) -->
@@ -373,12 +374,13 @@
         <item>ha-NG</item> <!-- Hausa (Nigeria) -->
         <item>haw-US</item> <!-- Hawaiian (United States) -->
         <item>hi-IN</item> <!-- Hindi (India) -->
+        <item>hi-Latn-IN</item> <!-- Hindi (Latin, India) -->
         <item>hr-BA</item> <!-- Croatian (Bosnia & Herzegovina) -->
         <item>hr-HR</item> <!-- Croatian (Croatia) -->
         <item>hsb-DE</item> <!-- Upper Sorbian (Germany) -->
         <item>hu-HU</item> <!-- Hungarian (Hungary) -->
         <item>hy-AM</item> <!-- Armenian (Armenia) -->
-        <item>ia-001</item> <!-- Interlingua (World) -->
+        <item>ia-001</item> <!-- Interlingua (world) -->
         <item>ig-NG</item> <!-- Igbo (Nigeria) -->
         <item>ii-CN</item> <!-- Sichuan Yi (China) -->
         <item>in-ID</item> <!-- Indonesian (Indonesia) -->
@@ -397,6 +399,7 @@
         <item>kam-KE</item> <!-- Kamba (Kenya) -->
         <item>kde-TZ</item> <!-- Makonde (Tanzania) -->
         <item>kea-CV</item> <!-- Kabuverdianu (Cape Verde) -->
+        <item>kgp-BR</item> <!-- Kaingang (Brazil) -->
         <item>khq-ML</item> <!-- Koyra Chiini (Mali) -->
         <item>ki-KE</item> <!-- Kikuyu (Kenya) -->
         <item>kk-KZ</item> <!-- Kazakh (Kazakhstan) -->
@@ -408,6 +411,9 @@
         <item>ko-KP</item> <!-- Korean (North Korea) -->
         <item>ko-KR</item> <!-- Korean (South Korea) -->
         <item>kok-IN</item> <!-- Konkani (India) -->
+        <item>ks-Arab-IN</item> <!-- Kashmiri (Arabic, India) -->
+        <item>ks-Arab-IN-u-nu-latn</item> <!-- Kashmiri (Arabic, India, Western Digits) -->
+        <item>ks-Deva-IN</item> <!-- Kashmiri (Devanagari, India) -->
         <item>ksb-TZ</item> <!-- Shambala (Tanzania) -->
         <item>ksf-CM</item> <!-- Bafia (Cameroon) -->
         <item>ksh-DE</item> <!-- Colognian (Germany) -->
@@ -435,7 +441,7 @@
         <item>mg-MG</item> <!-- Malagasy (Madagascar) -->
         <item>mgh-MZ</item> <!-- Makhuwa-Meetto (Mozambique) -->
         <item>mgo-CM</item> <!-- Metaʼ (Cameroon) -->
-        <item>mi-NZ</item> <!-- Maori (New Zealand) -->
+        <item>mi-NZ</item> <!-- Māori (New Zealand) -->
         <item>mk-MK</item> <!-- Macedonian (North Macedonia) -->
         <item>ml-IN</item> <!-- Malayalam (India) -->
         <item>mn-MN</item> <!-- Mongolian (Mongolia) -->
@@ -500,6 +506,8 @@
         <item>qu-BO</item> <!-- Quechua (Bolivia) -->
         <item>qu-EC</item> <!-- Quechua (Ecuador) -->
         <item>qu-PE</item> <!-- Quechua (Peru) -->
+        <item>raj-IN</item> <!-- Rajasthani (India) -->
+        <item>raj-IN-u-nu-latn</item> <!-- Rajasthani (India, Western Digits) -->
         <item>rm-CH</item> <!-- Romansh (Switzerland) -->
         <item>rn-BI</item> <!-- Rundi (Burundi) -->
         <item>ro-MD</item> <!-- Romanian (Moldova) -->
@@ -514,11 +522,13 @@
         <item>rw-RW</item> <!-- Kinyarwanda (Rwanda) -->
         <item>rwk-TZ</item> <!-- Rwa (Tanzania) -->
         <item>sa-IN</item> <!-- Sanskrit (India) -->
-        <item>sah-RU</item> <!-- Sakha (Russia) -->
+        <item>sa-IN-u-nu-latn</item> <!-- Sanskrit (India, Western Digits) -->
+        <item>sah-RU</item> <!-- Yakut (Russia) -->
         <item>saq-KE</item> <!-- Samburu (Kenya) -->
         <item>sat-IN</item> <!-- Santali (India) -->
         <item>sat-IN-u-nu-latn</item> <!-- Santali (India, Western Digits) -->
         <item>sbp-TZ</item> <!-- Sangu (Tanzania) -->
+        <item>sc-IT</item> <!-- Sardinian (Italy) -->
         <item>sd-Arab-PK</item> <!-- Sindhi (Arabic, Pakistan) -->
         <item>sd-Arab-PK-u-nu-latn</item> <!-- Sindhi (Arabic, Pakistan, Western Digits) -->
         <item>sd-Deva-IN</item> <!-- Sindhi (Devanagari, India) -->
@@ -565,6 +575,8 @@
         <item>teo-UG</item> <!-- Teso (Uganda) -->
         <item>tg-TJ</item> <!-- Tajik (Tajikistan) -->
         <item>th-TH</item> <!-- Thai (Thailand) -->
+        <item>ti-ER</item> <!-- Tigrinya (Eritrea) -->
+        <item>ti-ET</item> <!-- Tigrinya (Ethiopia) -->
         <item>tk-TM</item> <!-- Turkmen (Turkmenistan) -->
         <item>to-TO</item> <!-- Tongan (Tonga) -->
         <item>tr-CY</item> <!-- Turkish (Cyprus) -->
@@ -586,10 +598,14 @@
         <item>vun-TZ</item> <!-- Vunjo (Tanzania) -->
         <item>wae-CH</item> <!-- Walser (Switzerland) -->
         <item>wo-SN</item> <!-- Wolof (Senegal) -->
+        <item>xh-ZA</item> <!-- Xhosa (South Africa) -->
         <item>xog-UG</item> <!-- Soga (Uganda) -->
         <item>yav-CM</item> <!-- Yangben (Cameroon) -->
         <item>yo-BJ</item> <!-- Yoruba (Benin) -->
         <item>yo-NG</item> <!-- Yoruba (Nigeria) -->
+        <item>yrl-BR</item> <!-- Nheengatu (Brazil) -->
+        <item>yrl-CO</item> <!-- Nheengatu (Colombia) -->
+        <item>yrl-VE</item> <!-- Nheengatu (Venezuela) -->
         <item>yue-Hans-CN</item> <!-- Cantonese (Simplified, China) -->
         <item>yue-Hant-HK</item> <!-- Cantonese (Traditional, Hong Kong) -->
         <item>zgh-MA</item> <!-- Standard Moroccan Tamazight (Morocco) -->
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 90141e5..0aeee10 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -118,6 +118,7 @@
     <public name="enableTextStylingShortcuts" />
     <public name="requiredDisplayCategory"/>
     <public name="removed_maxConcurrentSessionsCount" />
+    <public name="visualQueryDetectionService" />
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01cd0000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 8106e24..add7307 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6435,4 +6435,8 @@
     <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>
+    <!-- Toast message that is shown when the user unmutes the microphone from the keyboard. [CHAR LIMIT=TOAST] -->
+    <string name="mic_access_on_toast">Microphone is available</string>
+    <!-- Toast message that is shown when the user mutes the microphone from the keyboard. [CHAR LIMIT=TOAST] -->
+    <string name="mic_access_off_toast">Microphone is blocked</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e8304d8..14c751f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -836,6 +836,8 @@
   <java-symbol type="string" name="lockscreen_emergency_call" />
   <java-symbol type="string" name="lockscreen_return_to_call" />
   <java-symbol type="string" name="low_memory" />
+  <java-symbol type="string" name="mic_access_off_toast" />
+  <java-symbol type="string" name="mic_access_on_toast" />
   <java-symbol type="string" name="midnight" />
   <java-symbol type="string" name="mismatchPin" />
   <java-symbol type="string" name="mmiComplete" />
@@ -4397,6 +4399,9 @@
   <!-- Set to true to make assistant show in front of the dream/screensaver. -->
   <java-symbol type="bool" name="config_assistantOnTopOfDream"/>
 
+  <!-- Set to true to enable letterboxing on translucent activities. -->
+  <java-symbol type="bool" name="config_letterboxIsEnabledForTranslucentActivities" />
+
   <java-symbol type="string" name="config_overrideComponentUiPackage" />
 
   <java-symbol type="string" name="notification_channel_network_status" />
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
index 3f35e99..cabeb13 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
@@ -17,6 +17,7 @@
 
 import static org.junit.Assert.*;
 import static org.junit.Assume.*;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyInt;
@@ -335,8 +336,10 @@
         assertEquals(RadioManager.STATUS_OK, scanRet);
         assertEquals(RadioManager.STATUS_OK, cancelRet);
 
-        verify(mCallback, after(kCancelTimeoutMs).atMost(1)).onError(RadioTuner.ERROR_CANCELLED);
+        verify(mCallback, after(kCancelTimeoutMs).atMost(1))
+                .onTuneFailed(eq(RadioTuner.TUNER_RESULT_CANCELED), any());
         verify(mCallback, atMost(1)).onProgramInfoChanged(any());
+        Mockito.reset(mCallback);
     }
 
     @Test
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
index 2fa3f876..65e55a2 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
@@ -130,6 +130,18 @@
     };
 
     @Test
+    public void seek_forRadioTuner_throwsException() {
+        UnsupportedOperationException thrown = assertThrows(
+                UnsupportedOperationException.class, () -> {
+                    DEFAULT_RADIO_TUNER.seek(RadioTuner.DIRECTION_DOWN,
+                            /* skipSubChannel= */ false);
+                });
+
+        assertWithMessage("Exception for seeking on default radio tuner")
+                .that(thrown).hasMessageThat().contains("Seeking is not supported");
+    }
+
+    @Test
     public void getDynamicProgramList_forRadioTuner_returnsNull() {
         assertWithMessage("Dynamic program list obtained from default radio tuner")
                 .that(DEFAULT_RADIO_TUNER.getDynamicProgramList(new ProgramList.Filter())).isNull();
@@ -143,29 +155,45 @@
 
     @Test
     public void isConfigFlagSet_forRadioTuner_throwsException() {
-        assertThrows(UnsupportedOperationException.class, () -> {
-            DEFAULT_RADIO_TUNER.isConfigFlagSet(/* flag= */ 1);
-        });
+        UnsupportedOperationException thrown = assertThrows(
+                UnsupportedOperationException.class, () -> {
+                    DEFAULT_RADIO_TUNER.isConfigFlagSet(/* flag= */ 1);
+                });
+
+        assertWithMessage("Exception for isConfigFlagSet on default radio tuner")
+                .that(thrown).hasMessageThat().contains("isConfigFlagSet is not supported");
     }
 
     @Test
     public void setConfigFlag_forRadioTuner_throwsException() {
-        assertThrows(UnsupportedOperationException.class, () -> {
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> {
             DEFAULT_RADIO_TUNER.setConfigFlag(/* flag= */ 1, /* value= */ false);
         });
+
+        assertWithMessage("Exception for setting config flag on default radio tuner")
+                .that(thrown).hasMessageThat().contains("Setting config flag is not supported");
     }
 
     @Test
     public void setParameters_forRadioTuner_throwsException() {
-        assertThrows(UnsupportedOperationException.class, () -> {
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> {
             DEFAULT_RADIO_TUNER.setParameters(Map.of("testKey", "testValue"));
         });
+
+        assertWithMessage("Exception for setting parameters from default radio tuner")
+                .that(thrown).hasMessageThat().contains("Setting parameters is not supported");
     }
 
     @Test
     public void getParameters_forRadioTuner_throwsException() {
-        assertThrows(UnsupportedOperationException.class, () -> {
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> {
             DEFAULT_RADIO_TUNER.getParameters(List.of("testKey"));
         });
+
+        assertWithMessage("Exception for getting parameters from default radio tuner")
+                .that(thrown).hasMessageThat().contains("Getting parameters is not supported");
     }
 }
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
index 2b9de18..87f91fa 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
@@ -41,6 +41,8 @@
 import android.os.RemoteException;
 import android.util.ArraySet;
 
+import androidx.test.InstrumentationRegistry;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -54,6 +56,8 @@
 @RunWith(MockitoJUnitRunner.class)
 public final class ProgramListTest {
 
+    public final Context mContext = InstrumentationRegistry.getContext();
+
     private static final int CREATOR_ARRAY_SIZE = 3;
     private static final VerificationWithTimeout CALLBACK_TIMEOUT = timeout(/* millis= */ 500);
 
@@ -109,8 +113,6 @@
     @Mock
     private IRadioService mRadioServiceMock;
     @Mock
-    private Context mContextMock;
-    @Mock
     private ITuner mTunerMock;
     @Mock
     private RadioTuner.Callback mTunerCallbackMock;
@@ -477,7 +479,7 @@
     }
 
     private void createRadioTuner() throws Exception {
-        RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock);
+        RadioManager radioManager = new RadioManager(mContext, mRadioServiceMock);
         RadioManager.BandConfig band = new RadioManager.FmBandConfig(
                 new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM,
                         /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 200,
@@ -487,7 +489,7 @@
         doAnswer(invocation -> {
             mTunerCallback = (ITunerCallback) invocation.getArguments()[3];
             return mTunerMock;
-        }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any());
+        }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any(), anyInt());
 
         mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, band,
                 /* withAudio= */ true, mTunerCallbackMock, /* handler= */ null);
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
index 44aa6d1..03742eb 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
@@ -18,14 +18,16 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.hardware.radio.Announcement;
 import android.hardware.radio.IAnnouncementListener;
 import android.hardware.radio.ICloseHandle;
@@ -34,6 +36,7 @@
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
 import android.hardware.radio.RadioTuner;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.RemoteException;
 import android.util.ArrayMap;
@@ -53,6 +56,8 @@
 @RunWith(MockitoJUnitRunner.class)
 public final class RadioManagerTest {
 
+    private static final int TEST_TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
     private static final int REGION = RadioManager.REGION_ITU_2;
     private static final int FM_LOWER_LIMIT = 87500;
     private static final int FM_UPPER_LIMIT = 108000;
@@ -126,6 +131,7 @@
                     /* vendorInfo= */ new ArrayMap<>()));
 
     private RadioManager mRadioManager;
+    private final ApplicationInfo mApplicationInfo = new ApplicationInfo();
 
     @Mock
     private IRadioService mRadioServiceMock;
@@ -1008,7 +1014,8 @@
         mRadioManager.openTuner(moduleId, FM_BAND_CONFIG, withAudio, mCallbackMock,
                 /* handler= */ null);
 
-        verify(mRadioServiceMock).openTuner(eq(moduleId), eq(FM_BAND_CONFIG), eq(withAudio), any());
+        verify(mRadioServiceMock).openTuner(eq(moduleId), eq(FM_BAND_CONFIG), eq(withAudio), any(),
+                anyInt());
     }
 
     @Test
@@ -1103,6 +1110,8 @@
     }
 
     private void createRadioManager() throws RemoteException {
+        mApplicationInfo.targetSdkVersion = TEST_TARGET_SDK_VERSION;
+        when(mContextMock.getApplicationInfo()).thenReturn(mApplicationInfo);
         when(mRadioServiceMock.listModules()).thenReturn(Arrays.asList(AMFM_PROPERTIES));
         when(mRadioServiceMock.addAnnouncementListener(any(), any())).thenReturn(mCloseHandleMock);
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
index bdba6a1..d851a7724 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.graphics.Bitmap;
 import android.hardware.radio.IRadioService;
 import android.hardware.radio.ITuner;
@@ -35,6 +36,7 @@
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
 import android.hardware.radio.RadioTuner;
+import android.os.Build;
 
 import org.junit.After;
 import org.junit.Before;
@@ -51,11 +53,12 @@
 @RunWith(MockitoJUnitRunner.class)
 public final class TunerAdapterTest {
 
+    private static final int TEST_TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
     private static final int CALLBACK_TIMEOUT_MS = 30_000;
     private static final int AM_LOWER_LIMIT_KHZ = 150;
 
     private static final RadioManager.BandConfig TEST_BAND_CONFIG = createBandConfig();
-
     private static final ProgramSelector.Identifier FM_IDENTIFIER =
             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
                     /* value= */ 94300);
@@ -66,6 +69,7 @@
 
     private RadioTuner mRadioTuner;
     private ITunerCallback mTunerCallback;
+    private final ApplicationInfo mApplicationInfo = new ApplicationInfo();
 
     @Mock
     private IRadioService mRadioServiceMock;
@@ -78,12 +82,14 @@
 
     @Before
     public void setUp() throws Exception {
+        mApplicationInfo.targetSdkVersion = TEST_TARGET_SDK_VERSION;
+        when(mContextMock.getApplicationInfo()).thenReturn(mApplicationInfo);
         RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock);
 
         doAnswer(invocation -> {
             mTunerCallback = (ITunerCallback) invocation.getArguments()[3];
             return mTunerMock;
-        }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any());
+        }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any(), anyInt());
 
         doAnswer(invocation -> {
             ProgramSelector program = (ProgramSelector) invocation.getArguments()[0];
@@ -92,7 +98,7 @@
                 throw new IllegalArgumentException();
             }
             if (program.getPrimaryId().getValue() < AM_LOWER_LIMIT_KHZ) {
-                mTunerCallback.onTuneFailed(RadioManager.STATUS_BAD_VALUE, program);
+                mTunerCallback.onTuneFailed(RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS, program);
             } else {
                 mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
             }
@@ -170,15 +176,30 @@
     }
 
     @Test
+    public void scan_forTunerAdapter_succeeds() throws Exception {
+        doAnswer(invocation -> {
+            mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
+            return RadioManager.STATUS_OK;
+        }).when(mTunerMock).seek(anyBoolean(), anyBoolean());
+
+        int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
+
+        verify(mTunerMock).seek(/* directionDown= */ true, /* skipSubChannel= */ false);
+        assertWithMessage("Status for scaning")
+                .that(scanStatus).isEqualTo(RadioManager.STATUS_OK);
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+    }
+
+    @Test
     public void seek_forTunerAdapter_succeeds() throws Exception {
         doAnswer(invocation -> {
             mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
             return RadioManager.STATUS_OK;
-        }).when(mTunerMock).scan(anyBoolean(), anyBoolean());
+        }).when(mTunerMock).seek(anyBoolean(), anyBoolean());
 
         int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
 
-        verify(mTunerMock).scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+        verify(mTunerMock).seek(/* directionDown= */ true, /* skipSubChannel= */ false);
         assertWithMessage("Status for seeking")
                 .that(scanStatus).isEqualTo(RadioManager.STATUS_OK);
         verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
@@ -187,13 +208,14 @@
     @Test
     public void seek_forTunerAdapter_invokesOnErrorWhenTimeout() throws Exception {
         doAnswer(invocation -> {
-            mTunerCallback.onError(RadioTuner.ERROR_SCAN_TIMEOUT);
+            mTunerCallback.onTuneFailed(RadioTuner.TUNER_RESULT_TIMEOUT, FM_SELECTOR);
             return RadioManager.STATUS_OK;
-        }).when(mTunerMock).scan(anyBoolean(), anyBoolean());
+        }).when(mTunerMock).seek(anyBoolean(), anyBoolean());
 
         mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel*/ true);
 
-        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_SCAN_TIMEOUT);
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onTuneFailed(
+                RadioTuner.TUNER_RESULT_TIMEOUT, FM_SELECTOR);
     }
 
     @Test
@@ -224,7 +246,7 @@
         mRadioTuner.tune(invalidSelector);
 
         verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
-                .onTuneFailed(RadioManager.STATUS_BAD_VALUE, invalidSelector);
+                .onTuneFailed(RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS, invalidSelector);
     }
 
     @Test
@@ -415,6 +437,17 @@
     }
 
     @Test
+    public void onConfigFlagUpdated_forTunerCallbackAdapter() throws Exception {
+        int configFlag = RadioManager.CONFIG_RDS_AF;
+        boolean configFlagValue = true;
+
+        mTunerCallback.onConfigFlagUpdated(configFlag, configFlagValue);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+                .onConfigFlagUpdated(configFlag, configFlagValue);
+    }
+
+    @Test
     public void onParametersUpdated_forTunerCallbackAdapter() throws Exception {
         Map<String, String> parametersExpected = Map.of("ParameterKeyMock", "ParameterValueMock");
 
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
index a2df426..9803474 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -33,6 +34,7 @@
 import android.hardware.radio.ITuner;
 import android.hardware.radio.ITunerCallback;
 import android.hardware.radio.RadioManager;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.ServiceManager;
 
@@ -55,6 +57,7 @@
             "android.hardware.broadcastradio.IBroadcastRadio/amfm";
     private static final String DAB_SERVICE_NAME =
             "android.hardware.broadcastradio.IBroadcastRadio/dab";
+    private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
 
     private IRadioServiceAidlImpl mAidlImpl;
 
@@ -82,7 +85,7 @@
         doNothing().when(mServiceMock).enforcePolicyAccess();
 
         when(mHalMock.listModules()).thenReturn(List.of(mModuleMock));
-        when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any()))
+        when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any(), eq(TARGET_SDK_VERSION)))
                 .thenReturn(mTunerMock);
         when(mHalMock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
 
@@ -114,7 +117,7 @@
     @Test
     public void openTuner_forAidlImpl() throws Exception {
         ITuner tuner = mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock,
-                /* withAudio= */ true, mTunerCallbackMock);
+                /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION);
 
         assertWithMessage("Tuner opened in AIDL HAL")
                 .that(tuner).isEqualTo(mTunerMock);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
index 5ab9435..cfff477 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
@@ -32,6 +32,7 @@
 import android.hardware.radio.ITuner;
 import android.hardware.radio.ITunerCallback;
 import android.hardware.radio.RadioManager;
+import android.os.Build;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -49,6 +50,7 @@
 
     private static final int HAL1_MODULE_ID = 0;
     private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+    private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
 
     private IRadioServiceHidlImpl mHidlImpl;
 
@@ -103,7 +105,7 @@
     @Test
     public void openTuner_withHal1ModuleId_forHidlImpl() throws Exception {
         ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID, mBandConfigMock,
-                /* withAudio= */ true, mTunerCallbackMock);
+                /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION);
 
         assertWithMessage("Tuner opened in HAL 1")
                 .that(tuner).isEqualTo(mHal1TunerMock);
@@ -112,7 +114,7 @@
     @Test
     public void openTuner_withHal2ModuleId_forHidlImpl() throws Exception {
         ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID + 1, mBandConfigMock,
-                /* withAudio= */ true, mTunerCallbackMock);
+                /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION);
 
         assertWithMessage("Tuner opened in HAL 2")
                 .that(tuner).isEqualTo(mHal2TunerMock);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java
index 1cc0a985..f404082 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java
@@ -36,6 +36,7 @@
 import android.hardware.radio.ITunerCallback;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.IServiceCallback;
 import android.os.RemoteException;
@@ -54,6 +55,8 @@
 
 public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTestCase {
 
+    private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
     private static final int FM_RADIO_MODULE_ID = 0;
     private static final int DAB_RADIO_MODULE_ID = 1;
     private static final ArrayList<String> SERVICE_LIST =
@@ -137,7 +140,8 @@
         createBroadcastRadioService();
 
         ITuner session = mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
-                /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+                /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock,
+                TARGET_SDK_VERSION);
 
         assertWithMessage("Session opened in FM radio module")
                 .that(session).isEqualTo(mFmTunerSessionMock);
@@ -148,7 +152,8 @@
         createBroadcastRadioService();
 
         ITuner session = mBroadcastRadioService.openSession(DAB_RADIO_MODULE_ID + 1,
-                /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+                /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock,
+                TARGET_SDK_VERSION);
 
         assertWithMessage("Session opened with id not found").that(session).isNull();
     }
@@ -160,7 +165,8 @@
 
         IllegalStateException thrown = assertThrows(IllegalStateException.class,
                 () -> mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
-                        /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock));
+                        /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock,
+                        TARGET_SDK_VERSION));
 
         assertWithMessage("Exception for opening session by non-current user")
                 .that(thrown).hasMessageThat().contains("Cannot open session for non-current user");
@@ -228,7 +234,7 @@
             return null;
         }).when(mFmBinderMock).linkToDeath(any(), anyInt());
 
-        when(mFmRadioModuleMock.openSession(eq(mTunerCallbackMock)))
+        when(mFmRadioModuleMock.openSession(eq(mTunerCallbackMock), eq(TARGET_SDK_VERSION)))
                 .thenReturn(mFmTunerSessionMock);
     }
 }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsResultTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsResultTest.java
new file mode 100644
index 0000000..df3ddfd
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsResultTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.broadcastradio.aidl;
+
+import android.hardware.broadcastradio.Result;
+import android.hardware.radio.RadioTuner;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public final class ConversionUtilsResultTest {
+
+    private final int mHalResult;
+    private final int mTunerResult;
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    public ConversionUtilsResultTest(int halResult, int tunerResult) {
+        this.mHalResult = halResult;
+        this.mTunerResult = tunerResult;
+    }
+
+    @Parameterized.Parameters
+    public static List<Object[]> inputParameters() {
+        return Arrays.asList(new Object[][]{
+                {Result.OK, RadioTuner.TUNER_RESULT_OK},
+                {Result.INTERNAL_ERROR, RadioTuner.TUNER_RESULT_INTERNAL_ERROR},
+                {Result.INVALID_ARGUMENTS, RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS},
+                {Result.INVALID_STATE, RadioTuner.TUNER_RESULT_INVALID_STATE},
+                {Result.NOT_SUPPORTED, RadioTuner.TUNER_RESULT_NOT_SUPPORTED},
+                {Result.TIMEOUT, RadioTuner.TUNER_RESULT_TIMEOUT},
+                {Result.UNKNOWN_ERROR, RadioTuner.TUNER_RESULT_UNKNOWN_ERROR}
+        });
+    }
+
+    @Test
+    public void halResultToTunerResult() {
+        expect.withMessage("Tuner result converted from AIDL HAL result %s", mHalResult)
+                .that(ConversionUtils.halResultToTunerResult(mHalResult))
+                .isEqualTo(mTunerResult);
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
index 3119554..a1cebb6 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
@@ -25,6 +25,7 @@
 import android.hardware.radio.Announcement;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
+import android.os.Build;
 
 import com.google.common.truth.Expect;
 
@@ -69,6 +70,18 @@
     public final Expect expect = Expect.create();
 
     @Test
+    public void isAtLeastU_withTSdkVersion_returnsFalse() {
+        expect.withMessage("Target SDK version of T")
+                .that(ConversionUtils.isAtLeastU(Build.VERSION_CODES.TIRAMISU)).isFalse();
+    }
+
+    @Test
+    public void isAtLeastU_withCurrentSdkVersion_returnsTrue() {
+        expect.withMessage("Target SDK version of U")
+                .that(ConversionUtils.isAtLeastU(Build.VERSION_CODES.CUR_DEVELOPMENT)).isTrue();
+    }
+
+    @Test
     public void propertiesFromHalProperties_idsMatch() {
         expect.withMessage("Properties id")
                 .that(MODULE_PROPERTIES.getId()).isEqualTo(TEST_ID);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
index 993ca77..c5c6349 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
@@ -42,6 +42,7 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
+import android.os.Build;
 import android.os.ServiceSpecificException;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -65,6 +66,7 @@
  */
 public final class TunerSessionTest extends ExtendedRadioMockitoTestCase {
 
+    private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
     private static final VerificationWithTimeout CALLBACK_TIMEOUT =
             timeout(/* millis= */ 200);
     private static final int SIGNAL_QUALITY = 1;
@@ -299,6 +301,18 @@
     }
 
     @Test
+    public void tune_withLowerSdkVersion() throws Exception {
+        openAidlClients(/* numClients= */ 1, Build.VERSION_CODES.TIRAMISU);
+        ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+        RadioManager.ProgramInfo tuneInfo =
+                AidlTestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY);
+
+        mTunerSessions[0].tune(initialSel);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onCurrentProgramInfoChanged(tuneInfo);
+    }
+
+    @Test
     public void tune_withMultipleSessions() throws Exception {
         int numSessions = 3;
         openAidlClients(numSessions);
@@ -377,49 +391,49 @@
     }
 
     @Test
-    public void scan_withDirectionUp() throws Exception {
+    public void seek_withDirectionUp() throws Exception {
         long initFreq = AM_FM_FREQUENCY_LIST[2];
         ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
-        RadioManager.ProgramInfo scanUpInfo = AidlTestUtils.makeProgramInfo(
+        RadioManager.ProgramInfo seekUpInfo = AidlTestUtils.makeProgramInfo(
                 AidlTestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ false)),
                 SIGNAL_QUALITY);
         openAidlClients(/* numClients= */ 1);
         mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
                 ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
 
-        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+        mTunerSessions[0].seek(/* directionDown= */ false, /* skipSubChannel= */ false);
 
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
-                .onCurrentProgramInfoChanged(scanUpInfo);
+                .onCurrentProgramInfoChanged(seekUpInfo);
     }
 
     @Test
-    public void scan_callsOnTuneFailedWhenTimeout() throws Exception {
+    public void seek_callsOnTuneFailedWhenTimeout() throws Exception {
         int numSessions = 2;
         openAidlClients(numSessions);
 
-        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+        mTunerSessions[0].seek(/* directionDown= */ false, /* skipSubChannel= */ false);
 
         for (int index = 0; index < numSessions; index++) {
             verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
-                    .onTuneFailed(eq(Result.TIMEOUT), any());
+                    .onTuneFailed(eq(RadioTuner.TUNER_RESULT_TIMEOUT), any());
         }
     }
 
     @Test
-    public void scan_withDirectionDown() throws Exception {
+    public void seek_withDirectionDown() throws Exception {
         long initFreq = AM_FM_FREQUENCY_LIST[2];
         ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
-        RadioManager.ProgramInfo scanUpInfo = AidlTestUtils.makeProgramInfo(
+        RadioManager.ProgramInfo seekUpInfo = AidlTestUtils.makeProgramInfo(
                 AidlTestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
                 SIGNAL_QUALITY);
         openAidlClients(/* numClients= */ 1);
         mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
                 ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
 
-        mTunerSessions[0].scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+        mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false);
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
-                .onCurrentProgramInfoChanged(scanUpInfo);
+                .onCurrentProgramInfoChanged(seekUpInfo);
     }
 
     @Test
@@ -585,7 +599,7 @@
     }
 
     @Test
-    public void onConfigFlagUpdated_forTunerCallback() throws Exception {
+    public void onAntennaStateChange_forTunerCallback() throws Exception {
         int numSessions = 3;
         openAidlClients(numSessions);
 
@@ -598,6 +612,21 @@
     }
 
     @Test
+    public void onConfigFlagUpdated_forTunerCallback() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+        boolean configFlagValue = true;
+
+        mHalTunerCallback.onConfigFlagUpdated(flag, configFlagValue);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onConfigFlagUpdated(flag, configFlagValue);
+        }
+    }
+
+    @Test
     public void onParametersUpdated_forTunerCallback() throws Exception {
         int numSessions = 3;
         openAidlClients(numSessions);
@@ -612,13 +641,17 @@
                     .onParametersUpdated(parametersExpected);
         }
     }
-
     private void openAidlClients(int numClients) throws Exception {
+        openAidlClients(numClients, TARGET_SDK_VERSION);
+    }
+
+    private void openAidlClients(int numClients, int targetSdkVersion) throws Exception {
         mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
         mTunerSessions = new TunerSession[numClients];
         for (int index = 0; index < numClients; index++) {
             mAidlTunerCallbackMocks[index] = mock(android.hardware.radio.ITunerCallback.class);
-            mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index]);
+            mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index],
+                    targetSdkVersion);
         }
     }
 
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertResultTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertResultTest.java
new file mode 100644
index 0000000..d106de9
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertResultTest.java
@@ -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.server.broadcastradio.hal2;
+
+import android.hardware.broadcastradio.V2_0.Result;
+import android.hardware.radio.RadioTuner;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public final class ConvertResultTest {
+
+    private final int mHalResult;
+    private final int mTunerResult;
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    public ConvertResultTest(int halResult, int tunerResult) {
+        this.mHalResult = halResult;
+        this.mTunerResult = tunerResult;
+    }
+
+    @Parameterized.Parameters
+    public static List<Object[]> inputParameters() {
+        return Arrays.asList(new Object[][]{
+                {Result.OK, RadioTuner.TUNER_RESULT_OK},
+                {Result.INTERNAL_ERROR, RadioTuner.TUNER_RESULT_INTERNAL_ERROR},
+                {Result.INVALID_ARGUMENTS, RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS},
+                {Result.INVALID_STATE, RadioTuner.TUNER_RESULT_INVALID_STATE},
+                {Result.NOT_SUPPORTED, RadioTuner.TUNER_RESULT_NOT_SUPPORTED},
+                {Result.TIMEOUT, RadioTuner.TUNER_RESULT_TIMEOUT},
+                {Result.UNKNOWN_ERROR, RadioTuner.TUNER_RESULT_UNKNOWN_ERROR}
+        });
+    }
+
+    @Test
+    public void halResultToTunerResult() {
+        expect.withMessage("Tuner result converted from HAL 2.0 result %s",
+                Result.toString(mHalResult))
+                .that(Convert.halResultToTunerResult(mHalResult))
+                .isEqualTo(mTunerResult);
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
index ff988a2..db16c03 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
@@ -384,49 +384,49 @@
     }
 
     @Test
-    public void scan_withDirectionUp() throws Exception {
+    public void seek_withDirectionUp() throws Exception {
         long initFreq = AM_FM_FREQUENCY_LIST[2];
         ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
-        RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo(
+        RadioManager.ProgramInfo seekUpInfo = TestUtils.makeProgramInfo(
                 TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ false)),
                 SIGNAL_QUALITY);
         openAidlClients(/* numClients= */ 1);
         mHalCurrentInfo = TestUtils.makeHalProgramInfo(
                 Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
 
-        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+        mTunerSessions[0].seek(/* directionDown= */ false, /* skipSubChannel= */ false);
 
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
-                .onCurrentProgramInfoChanged(scanUpInfo);
+                .onCurrentProgramInfoChanged(seekUpInfo);
     }
 
     @Test
-    public void scan_callsOnTuneFailedWhenTimeout() throws Exception {
+    public void seek_callsOnTuneFailedWhenTimeout() throws Exception {
         int numSessions = 2;
         openAidlClients(numSessions);
 
-        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+        mTunerSessions[0].seek(/* directionDown= */ false, /* skipSubChannel= */ false);
 
         for (int index = 0; index < numSessions; index++) {
             verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
-                    .onTuneFailed(eq(Result.TIMEOUT), any());
+                    .onTuneFailed(eq(RadioTuner.TUNER_RESULT_TIMEOUT), any());
         }
     }
 
     @Test
-    public void scan_withDirectionDown() throws Exception {
+    public void seek_withDirectionDown() throws Exception {
         long initFreq = AM_FM_FREQUENCY_LIST[2];
         ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
-        RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo(
+        RadioManager.ProgramInfo seekUpInfo = TestUtils.makeProgramInfo(
                 TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
                 SIGNAL_QUALITY);
         openAidlClients(/* numClients= */ 1);
         mHalCurrentInfo = TestUtils.makeHalProgramInfo(
                 Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
 
-        mTunerSessions[0].scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+        mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false);
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
-                .onCurrentProgramInfoChanged(scanUpInfo);
+                .onCurrentProgramInfoChanged(seekUpInfo);
     }
 
     @Test
diff --git a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
index 7674135..8f83461 100644
--- a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
@@ -20,7 +20,6 @@
 
 import static com.android.internal.jank.FrameTracker.REASON_CANCEL_TIMEOUT;
 import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_LAUNCH_CAMERA;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_ADD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_APP_START;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_HEADS_UP_APPEAR;
@@ -32,7 +31,6 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_EXPAND;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_SWIPE;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_TO_STATSD_INTERACTION_TYPE;
 import static com.android.internal.jank.InteractionJankMonitor.MAX_LENGTH_OF_CUJ_NAME;
 import static com.android.internal.jank.InteractionJankMonitor.getNameOfCuj;
@@ -92,7 +90,6 @@
 public class InteractionJankMonitorTest {
     private static final String CUJ_POSTFIX = "";
     private static final SparseArray<String> ENUM_NAME_EXCEPTION_MAP = new SparseArray<>();
-    private static final SparseArray<String> CUJ_NAME_EXCEPTION_MAP = new SparseArray<>();
     private static final String ENUM_NAME_PREFIX =
             "UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__";
 
@@ -129,22 +126,6 @@
                 CUJ_NOTIFICATION_SHADE_ROW_SWIPE, getEnumName("SHADE_ROW_SWIPE"));
         ENUM_NAME_EXCEPTION_MAP.put(
                 CUJ_NOTIFICATION_SHADE_SCROLL_FLING, getEnumName("SHADE_SCROLL_FLING"));
-
-        CUJ_NAME_EXCEPTION_MAP.put(
-                CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, "CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE");
-        CUJ_NAME_EXCEPTION_MAP.put(
-                CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
-                "CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE");
-        CUJ_NAME_EXCEPTION_MAP.put(
-                CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE, "CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE");
-        CUJ_NAME_EXCEPTION_MAP.put(
-                CUJ_NOTIFICATION_SHADE_ROW_EXPAND, "CUJ_NOTIFICATION_SHADE_ROW_EXPAND");
-        CUJ_NAME_EXCEPTION_MAP.put(
-                CUJ_NOTIFICATION_SHADE_ROW_SWIPE, "CUJ_NOTIFICATION_SHADE_ROW_SWIPE");
-        CUJ_NAME_EXCEPTION_MAP.put(
-                CUJ_NOTIFICATION_SHADE_SCROLL_FLING, "CUJ_NOTIFICATION_SHADE_SCROLL_FLING");
-        CUJ_NAME_EXCEPTION_MAP.put(CUJ_LOCKSCREEN_LAUNCH_CAMERA, "CUJ_LOCKSCREEN_LAUNCH_CAMERA");
-        CUJ_NAME_EXCEPTION_MAP.put(CUJ_SPLIT_SCREEN_RESIZE, "CUJ_SPLIT_SCREEN_RESIZE");
     }
 
     private static String getEnumName(String name) {
@@ -272,9 +253,8 @@
                     : formatSimple("%s%s", ENUM_NAME_PREFIX, cujName.substring(4));
             final int enumKey = CUJ_TO_STATSD_INTERACTION_TYPE[cuj];
             final String enumName = enumsMap.get(enumKey);
-            final String expectedNameOfCuj = CUJ_NAME_EXCEPTION_MAP.contains(cuj)
-                    ? CUJ_NAME_EXCEPTION_MAP.get(cuj)
-                    : formatSimple("CUJ_%s", getNameOfCuj(cuj));
+            final String expectedNameOfCuj = formatSimple("CUJ_%s", getNameOfCuj(cuj));
+
             mExpect
                     .withMessage(formatSimple(
                             "%s (%d) not matches %s (%d)", cujName, cuj, enumName, enumKey))
@@ -323,7 +303,7 @@
         // Since the length of the cuj name is tested in another test, no need to test it here.
         // Too long postfix case, should trim the postfix and keep the cuj name completed.
         final String expectedTrimmedName = formatSimple("J<%s::%s>", cujName,
-                "ThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagT...");
+                "ThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagThi...");
         Session longPostfix = new Session(cujType, tooLongTag);
         assertThat(longPostfix.getName()).isEqualTo(expectedTrimmedName);
     }
diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
index 4679a9e..0b70199 100644
--- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
@@ -19,6 +19,9 @@
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
 
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertFalse;
@@ -37,6 +40,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.pm.UserInfo;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -233,6 +237,45 @@
                 ComponentName.unflattenFromString("com.test/.TestAgent"));
     }
 
+    @Test
+    public void isBiometricAllowedForUser_afterTrustagentExpired_returnsTrue()
+            throws RemoteException {
+        TestStrongAuthTracker tracker = createStrongAuthTracker();
+        tracker.changeStrongAuth(SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED);
+
+        assertTrue(tracker.isBiometricAllowedForUser(
+                /* isStrongBiometric = */ true,
+                DEMO_USER_ID));
+    }
+
+    @Test
+    public void isBiometricAllowedForUser_afterLockout_returnsFalse()
+            throws RemoteException {
+        TestStrongAuthTracker tracker = createStrongAuthTracker();
+        tracker.changeStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
+
+        assertFalse(tracker.isBiometricAllowedForUser(
+                /* isStrongBiometric = */ true,
+                DEMO_USER_ID));
+    }
+
+
+    private TestStrongAuthTracker createStrongAuthTracker() {
+        final Context context = new ContextWrapper(InstrumentationRegistry.getTargetContext());
+        return new TestStrongAuthTracker(context, Looper.getMainLooper());
+    }
+
+    private static class TestStrongAuthTracker extends LockPatternUtils.StrongAuthTracker {
+
+        TestStrongAuthTracker(Context context, Looper looper) {
+            super(context, looper);
+        }
+
+        public void changeStrongAuth(@StrongAuthFlags int strongAuthFlags) {
+            handleStrongAuthRequiredChanged(strongAuthFlags, DEMO_USER_ID);
+        }
+    }
+
     private ILockSettings createTestLockSettings() {
         final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         mLockPatternUtils = spy(new LockPatternUtils(context));
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 73f25b1..1ab5e4b 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -169,6 +169,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-1944652783": {
+      "message": "Unable to tell MediaProjectionManagerService to stop the active projection: %s",
+      "level": "ERROR",
+      "group": "WM_DEBUG_CONTENT_RECORDING",
+      "at": "com\/android\/server\/wm\/ContentRecorder.java"
+    },
     "-1941440781": {
       "message": "Creating Pending Move-to-back: %s",
       "level": "VERBOSE",
@@ -619,6 +625,12 @@
       "group": "WM_DEBUG_CONFIGURATION",
       "at": "com\/android\/server\/wm\/ActivityStarter.java"
     },
+    "-1484988952": {
+      "message": "Creating Pending Multiwindow Fullscreen Request: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/ActivityClientController.java"
+    },
     "-1483435730": {
       "message": "InsetsSource setWin %s for type %s",
       "level": "DEBUG",
@@ -709,6 +721,12 @@
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-1423223548": {
+      "message": "Unable to tell MediaProjectionManagerService about resizing the active projection: %s",
+      "level": "ERROR",
+      "group": "WM_DEBUG_CONTENT_RECORDING",
+      "at": "com\/android\/server\/wm\/ContentRecorder.java"
+    },
     "-1421296808": {
       "message": "Moving to RESUMED: %s (in existing)",
       "level": "VERBOSE",
diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl
index a43e225..0f8616d 100644
--- a/data/keyboards/Generic.kl
+++ b/data/keyboards/Generic.kl
@@ -139,7 +139,7 @@
 key 117   NUMPAD_EQUALS
 # key 118 "KEY_KPPLUSMINUS"
 key 119   BREAK
-# key 120 (undefined)
+key 120   RECENT_APPS
 key 121   NUMPAD_COMMA
 key 122   KANA
 key 123   EISU
diff --git a/identity/java/android/security/identity/AuthenticationKeyMetadata.java b/identity/java/android/security/identity/AuthenticationKeyMetadata.java
index d4c28f8..c6abc22 100644
--- a/identity/java/android/security/identity/AuthenticationKeyMetadata.java
+++ b/identity/java/android/security/identity/AuthenticationKeyMetadata.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -24,11 +24,11 @@
 /**
  * Data about authentication keys.
  */
-public class AuthenticationKeyMetadata {
+public final class AuthenticationKeyMetadata {
     private int mUsageCount;
     private Instant mExpirationDate;
 
-    AuthenticationKeyMetadata(int usageCount, Instant expirationDate) {
+    AuthenticationKeyMetadata(int usageCount, @NonNull Instant expirationDate) {
         mUsageCount = usageCount;
         mExpirationDate = expirationDate;
     }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 5afb1d1..5400164 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -431,12 +431,8 @@
         if (container != null) {
             // Cleanup if the TaskFragment vanished is not requested by the organizer.
             removeContainer(container);
-            // Make sure the top container is updated.
-            final TaskFragmentContainer newTopContainer = getTopActiveContainer(
-                    container.getTaskId());
-            if (newTopContainer != null) {
-                updateContainer(wct, newTopContainer);
-            }
+            // Make sure the containers in the Task are up-to-date.
+            updateContainersInTaskIfVisible(wct, container.getTaskId());
         }
         cleanupTaskFragment(taskFragmentInfo.getFragmentToken());
     }
@@ -476,6 +472,13 @@
         updateContainersInTask(wct, taskContainer);
     }
 
+    void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) {
+        final TaskContainer taskContainer = getTaskContainer(taskId);
+        if (taskContainer != null && taskContainer.isVisible()) {
+            updateContainersInTask(wct, taskContainer);
+        }
+    }
+
     private void updateContainersInTask(@NonNull WindowContainerTransaction wct,
             @NonNull TaskContainer taskContainer) {
         // Update all TaskFragments in the Task. Make a copy of the list since some may be
@@ -1328,9 +1331,6 @@
     void removeContainer(@NonNull TaskFragmentContainer container) {
         // Remove all split containers that included this one
         final TaskContainer taskContainer = container.getTaskContainer();
-        if (taskContainer == null) {
-            return;
-        }
         taskContainer.mContainers.remove(container);
         // Marked as a pending removal which will be removed after it is actually removed on the
         // server side (#onTaskFragmentVanished).
@@ -1523,14 +1523,8 @@
         }
 
         final TaskFragmentContainer container = getContainerWithActivity(activity);
-        // Don't launch placeholder if the container is occluded.
-        if (container != null && container != getTopActiveContainer(container.getTaskId())) {
-            return false;
-        }
-
-        final SplitContainer splitContainer = getActiveSplitForContainer(container);
-        if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
-            // Don't launch placeholder in primary split container
+        if (container != null && !allowLaunchPlaceholder(container)) {
+            // We don't allow activity in this TaskFragment to launch placeholder.
             return false;
         }
 
@@ -1558,6 +1552,32 @@
         return true;
     }
 
+    /** Whether or not to allow activity in this container to launch placeholder. */
+    @GuardedBy("mLock")
+    private boolean allowLaunchPlaceholder(@NonNull TaskFragmentContainer container) {
+        final TaskFragmentContainer topContainer = getTopActiveContainer(container.getTaskId());
+        if (container != topContainer) {
+            // The container is not the top most.
+            if (!container.isVisible()) {
+                // In case the container is visible (the one on top may be transparent), we may
+                // still want to launch placeholder even if it is not the top most.
+                return false;
+            }
+            if (topContainer.isWaitingActivityAppear()) {
+                // When the top container appeared info is not sent by the server yet, the visible
+                // check above may not be reliable.
+                return false;
+            }
+        }
+
+        final SplitContainer splitContainer = getActiveSplitForContainer(container);
+        if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
+            // Don't launch placeholder for primary split container.
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Gets the activity options for starting the placeholder activity. In case the placeholder is
      * launched when the Task is in the background, we don't want to bring the Task to the front.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 47253d3..a432e2b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -154,12 +154,8 @@
     void cleanupContainer(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
         container.finish(shouldFinishDependent, this, wct, mController);
-
-        final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(
-                container.getTaskId());
-        if (newTopContainer != null) {
-            mController.updateContainer(wct, newTopContainer);
-        }
+        // Make sure the containers in the Task is up-to-date.
+        mController.updateContainersInTaskIfVisible(wct, container.getTaskId());
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index fcf0ac7..8240874 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -184,6 +184,11 @@
         return allActivities;
     }
 
+    /** Whether this TaskFragment is visible. */
+    boolean isVisible() {
+        return mInfo != null && mInfo.isVisible();
+    }
+
     /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/
     boolean isInIntermediateState() {
         if (mInfo == null) {
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index bc03e4e..2f92a57 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -163,11 +163,17 @@
     /** Creates a mock TaskFragmentInfo for the given TaskFragment. */
     static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
             @NonNull Activity activity) {
+        return createMockTaskFragmentInfo(container, activity, true /* isVisible */);
+    }
+
+    /** Creates a mock TaskFragmentInfo for the given TaskFragment. */
+    static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
+            @NonNull Activity activity, boolean isVisible) {
         return new TaskFragmentInfo(container.getTaskFragmentToken(),
                 mock(WindowContainerToken.class),
                 new Configuration(),
                 1,
-                true /* isVisible */,
+                isVisible,
                 Collections.singletonList(activity.getActivityToken()),
                 new Point(),
                 false /* isTaskClearedForReuse */,
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 6725dfd..3cc31f9 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -1254,6 +1254,68 @@
         verify(mEmbeddingCallback).accept(any());
     }
 
+    @Test
+    public void testLaunchPlaceholderIfNecessary_nonEmbeddedActivity() {
+        // Launch placeholder for non embedded activity.
+        setupPlaceholderRule(mActivity);
+        mTransactionManager.startNewTransaction();
+        mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+                true /* isOnCreated */);
+
+        verify(mTransaction).startActivityInTaskFragment(any(), any(), eq(PLACEHOLDER_INTENT),
+                any());
+    }
+
+    @Test
+    public void testLaunchPlaceholderIfNecessary_embeddedInTopTaskFragment() {
+        // Launch placeholder for activity in top TaskFragment.
+        setupPlaceholderRule(mActivity);
+        mTransactionManager.startNewTransaction();
+        final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID);
+        mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+                true /* isOnCreated */);
+
+        assertTrue(container.hasActivity(mActivity.getActivityToken()));
+        verify(mTransaction).startActivityInTaskFragment(any(), any(), eq(PLACEHOLDER_INTENT),
+                any());
+    }
+
+    @Test
+    public void testLaunchPlaceholderIfNecessary_embeddedBelowTaskFragment() {
+        // Do not launch placeholder for invisible activity below the top TaskFragment.
+        setupPlaceholderRule(mActivity);
+        mTransactionManager.startNewTransaction();
+        final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID);
+        final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity,
+                TASK_ID);
+        bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity,
+                false /* isVisible */));
+        topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
+        assertFalse(bottomTf.isVisible());
+        mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+                true /* isOnCreated */);
+
+        verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any());
+    }
+
+    @Test
+    public void testLaunchPlaceholderIfNecessary_embeddedBelowTransparentTaskFragment() {
+        // Launch placeholder for visible activity below the top TaskFragment.
+        setupPlaceholderRule(mActivity);
+        mTransactionManager.startNewTransaction();
+        final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID);
+        final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity,
+                TASK_ID);
+        bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity,
+                true /* isVisible */));
+        topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
+        assertTrue(bottomTf.isVisible());
+        mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+                true /* isOnCreated */);
+
+        verify(mTransaction).startActivityInTaskFragment(any(), any(), any(), any());
+    }
+
     /** Creates a mock activity in the organizer process. */
     private Activity createMockActivity() {
         return createMockActivity(TASK_ID);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 5c3ba72..9877236 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -500,6 +500,29 @@
         assertEquals(2, taskContainer.indexOf(tf1));
     }
 
+    @Test
+    public void testIsVisible() {
+        final TaskContainer taskContainer = createTestTaskContainer();
+        final TaskFragmentContainer container = new TaskFragmentContainer(
+                null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
+                null /* pairedPrimaryTaskFragment */);
+
+        // Not visible when there is not appeared.
+        assertFalse(container.isVisible());
+
+        // Respect info.isVisible.
+        TaskFragmentInfo info = createMockTaskFragmentInfo(container, mActivity,
+                true /* isVisible */);
+        container.setInfo(mTransaction, info);
+
+        assertTrue(container.isVisible());
+
+        info = createMockTaskFragmentInfo(container, mActivity, false /* isVisible */);
+        container.setInfo(mTransaction, info);
+
+        assertFalse(container.isVisible());
+    }
+
     /** Creates a mock activity in the organizer process. */
     private Activity createMockActivity() {
         final Activity activity = mock(Activity.class);
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 8881be7..36d3313 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -21,5 +21,6 @@
     <uses-permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
+    <uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" />
     <uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
 </manifest>
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
index 70755e6..dcce4698 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -44,67 +44,15 @@
             android:background="@color/tv_pip_menu_dim_layer"
             android:alpha="0"/>
 
-        <ScrollView
-            android:id="@+id/tv_pip_menu_scroll"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:scrollbars="none"
-            android:visibility="gone"/>
-
-        <HorizontalScrollView
-            android:id="@+id/tv_pip_menu_horizontal_scroll"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:scrollbars="none">
-
-            <LinearLayout
-                android:id="@+id/tv_pip_menu_action_buttons"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:alpha="0">
-
-                <Space
-                    android:layout_width="@dimen/pip_menu_button_wrapper_margin"
-                    android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
-
-                <com.android.wm.shell.common.TvWindowMenuActionButton
-                    android:id="@+id/tv_pip_menu_fullscreen_button"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:src="@drawable/pip_ic_fullscreen_white"
-                    android:text="@string/pip_fullscreen" />
-
-                <com.android.wm.shell.common.TvWindowMenuActionButton
-                    android:id="@+id/tv_pip_menu_close_button"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:src="@drawable/pip_ic_close_white"
-                    android:text="@string/pip_close" />
-
-                <!-- More TvWindowMenuActionButtons may be added here at runtime. -->
-
-                <com.android.wm.shell.common.TvWindowMenuActionButton
-                    android:id="@+id/tv_pip_menu_move_button"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:src="@drawable/pip_ic_move_white"
-                    android:text="@string/pip_move" />
-
-                <com.android.wm.shell.common.TvWindowMenuActionButton
-                    android:id="@+id/tv_pip_menu_expand_button"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:src="@drawable/pip_ic_collapse"
-                    android:visibility="gone"
-                    android:text="@string/pip_collapse" />
-
-                <Space
-                    android:layout_width="@dimen/pip_menu_button_wrapper_margin"
-                    android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
-
-            </LinearLayout>
-        </HorizontalScrollView>
+        <com.android.internal.widget.RecyclerView
+            android:id="@+id/tv_pip_menu_action_buttons"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:padding="@dimen/pip_menu_button_start_end_offset"
+            android:clipToPadding="false"
+            android:alpha="0"
+            android:contentDescription="@string/a11y_pip_menu_entered"/>
     </FrameLayout>
 
     <!-- Frame around the content, just overlapping the corners to make them round -->
diff --git a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml
index c4dbd39..b2ac85b 100644
--- a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml
@@ -21,6 +21,7 @@
     android:layout_width="@dimen/tv_window_menu_button_size"
     android:layout_height="@dimen/tv_window_menu_button_size"
     android:padding="@dimen/tv_window_menu_button_margin"
+    android:duplicateParentState="true"
     android:stateListAnimator="@animator/tv_window_menu_action_button_animator"
     android:focusable="true">
 
diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index 1a7cf3e..9a926d8 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -34,8 +34,7 @@
     <string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"दिखाएं"</string>
     <string name="dock_forced_resizable" msgid="1749750436092293116">"ऐप्लिकेशन शायद स्प्लिट स्क्रीन मोड में काम न करे."</string>
     <string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ऐप विभाजित स्‍क्रीन का समर्थन नहीं करता है."</string>
-    <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) -->
-    <skip />
+    <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"इस ऐप्लिकेशन को सिर्फ़ एक विंडो में खोला जा सकता है."</string>
     <string name="forced_resizable_secondary_display" msgid="1768046938673582671">"हो सकता है कि ऐप प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर काम न करे."</string>
     <string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर ऐप लॉन्च नहीं किया जा सकता."</string>
     <string name="accessibility_divider" msgid="703810061635792791">"विभाजित स्क्रीन विभाजक"</string>
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
index 9833a88..0b61d7a 100644
--- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
+++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
@@ -28,7 +28,7 @@
     <dimen name="pip_menu_background_corner_radius">6dp</dimen>
     <dimen name="pip_menu_border_width">4dp</dimen>
     <dimen name="pip_menu_outer_space">24dp</dimen>
-    <dimen name="pip_menu_button_wrapper_margin">26dp</dimen>
+    <dimen name="pip_menu_button_start_end_offset">30dp</dimen>
 
     <!-- outer space minus border width -->
     <dimen name="pip_menu_outer_space_frame">20dp</dimen>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java
index 39b0b55..8ba785a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java
@@ -19,6 +19,8 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -30,11 +32,11 @@
 /**
  * A common action button for TV window menu layouts.
  */
-public class TvWindowMenuActionButton extends RelativeLayout implements View.OnClickListener {
+public class TvWindowMenuActionButton extends RelativeLayout {
     private final ImageView mIconImageView;
     private final View mButtonBackgroundView;
-    private final View mButtonView;
-    private OnClickListener mOnClickListener;
+
+    private Icon mCurrentIcon;
 
     public TvWindowMenuActionButton(Context context) {
         this(context, null, 0, 0);
@@ -56,7 +58,6 @@
         inflater.inflate(R.layout.tv_window_menu_action_button, this);
 
         mIconImageView = findViewById(R.id.icon);
-        mButtonView = findViewById(R.id.button);
         mButtonBackgroundView = findViewById(R.id.background);
 
         final int[] values = new int[]{android.R.attr.src, android.R.attr.text};
@@ -71,23 +72,6 @@
         typedArray.recycle();
     }
 
-    @Override
-    public void setOnClickListener(OnClickListener listener) {
-        // We do not want to set an OnClickListener to the TvWindowMenuActionButton itself, but only
-        // to the ImageView. So let's "cash" the listener we've been passed here and set a "proxy"
-        // listener to the ImageView.
-        mOnClickListener = listener;
-        mButtonView.setOnClickListener(listener != null ? this : null);
-    }
-
-    @Override
-    public void onClick(View v) {
-        if (mOnClickListener != null) {
-            // Pass the correct view - this.
-            mOnClickListener.onClick(this);
-        }
-    }
-
     /**
      * Sets the drawable for the button with the given drawable.
      */
@@ -104,11 +88,24 @@
         }
     }
 
+    public void setImageIconAsync(Icon icon, Handler handler) {
+        mCurrentIcon = icon;
+        // Remove old image while waiting for the new one to load.
+        mIconImageView.setImageDrawable(null);
+        icon.loadDrawableAsync(mContext, d -> {
+            // The image hasn't been set any other way and the drawable belongs to the most
+            // recently set Icon.
+            if (mIconImageView.getDrawable() == null && mCurrentIcon == icon) {
+                mIconImageView.setImageDrawable(d);
+            }
+        }, handler);
+    }
+
     /**
      * Sets the text for description the with the given string.
      */
     public void setTextAndDescription(CharSequence text) {
-        mButtonView.setContentDescription(text);
+        setContentDescription(text);
     }
 
     /**
@@ -118,16 +115,6 @@
         setTextAndDescription(getContext().getString(resId));
     }
 
-    @Override
-    public void setEnabled(boolean enabled) {
-        mButtonView.setEnabled(enabled);
-    }
-
-    @Override
-    public boolean isEnabled() {
-        return mButtonView.isEnabled();
-    }
-
     /**
      * Marks this button as a custom close action button.
      * This changes the style of the action button to highlight that this action finishes the
@@ -147,10 +134,10 @@
 
     @Override
     public String toString() {
-        if (mButtonView.getContentDescription() == null) {
+        if (getContentDescription() == null) {
             return TvWindowMenuActionButton.class.getSimpleName();
         }
-        return mButtonView.getContentDescription().toString();
+        return getContentDescription().toString();
     }
 
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index 8022e9b..b144d22 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -81,6 +81,7 @@
             PipParamsChangedForwarder pipParamsChangedForwarder,
             DisplayController displayController,
             WindowManagerShellWrapper windowManagerShellWrapper,
+            @ShellMainThread Handler mainHandler, // needed for registerReceiverForAllUsers()
             @ShellMainThread ShellExecutor mainExecutor) {
         return Optional.of(
                 TvPipController.create(
@@ -100,6 +101,7 @@
                         pipParamsChangedForwarder,
                         displayController,
                         windowManagerShellWrapper,
+                        mainHandler,
                         mainExecutor));
     }
 
@@ -157,22 +159,17 @@
             Context context,
             TvPipBoundsState tvPipBoundsState,
             SystemWindows systemWindows,
-            PipMediaController pipMediaController,
             @ShellMainThread Handler mainHandler) {
-        return new TvPipMenuController(context, tvPipBoundsState, systemWindows, pipMediaController,
-                mainHandler);
+        return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler);
     }
 
-    // Handler needed for registerReceiverForAllUsers()
     @WMSingleton
     @Provides
     static TvPipNotificationController provideTvPipNotificationController(Context context,
             PipMediaController pipMediaController,
-            PipParamsChangedForwarder pipParamsChangedForwarder,
-            TvPipBoundsState tvPipBoundsState,
-            @ShellMainThread Handler mainHandler) {
+            PipParamsChangedForwarder pipParamsChangedForwarder) {
         return new TvPipNotificationController(context, pipMediaController,
-                pipParamsChangedForwarder, tvPipBoundsState, mainHandler);
+                pipParamsChangedForwarder);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index 62bf517..d93a901 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -240,7 +240,7 @@
             // Update launch options for the split side we are targeting.
             position = leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT;
             // Add some data for logging splitscreen once it is invoked
-            mSplitScreen.logOnDroppedToSplit(position, mLoggerSessionId);
+            mSplitScreen.onDroppedToSplit(position, mLoggerSessionId);
         }
 
         final ClipDescription description = data.getDescription();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java
new file mode 100644
index 0000000..222307f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.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 com.android.wm.shell.pip.tv;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.wm.shell.common.TvWindowMenuActionButton;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+abstract class TvPipAction {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"ACTION_"}, value = {
+            ACTION_FULLSCREEN,
+            ACTION_CLOSE,
+            ACTION_MOVE,
+            ACTION_EXPAND_COLLAPSE,
+            ACTION_CUSTOM,
+            ACTION_CUSTOM_CLOSE
+    })
+    public @interface ActionType {
+    }
+
+    public static final int ACTION_FULLSCREEN = 0;
+    public static final int ACTION_CLOSE = 1;
+    public static final int ACTION_MOVE = 2;
+    public static final int ACTION_EXPAND_COLLAPSE = 3;
+    public static final int ACTION_CUSTOM = 4;
+    public static final int ACTION_CUSTOM_CLOSE = 5;
+
+    @ActionType
+    private final int mActionType;
+
+    @NonNull
+    private final SystemActionsHandler mSystemActionsHandler;
+
+    TvPipAction(@ActionType int actionType, @NonNull SystemActionsHandler systemActionsHandler) {
+        Objects.requireNonNull(systemActionsHandler);
+        mActionType = actionType;
+        mSystemActionsHandler = systemActionsHandler;
+    }
+
+    boolean isCloseAction() {
+        return mActionType == ACTION_CLOSE || mActionType == ACTION_CUSTOM_CLOSE;
+    }
+
+    @ActionType
+    int getActionType() {
+        return mActionType;
+    }
+
+    abstract void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler);
+
+    abstract PendingIntent getPendingIntent();
+
+    void executeAction() {
+        mSystemActionsHandler.executeAction(mActionType);
+    }
+
+    abstract Notification.Action toNotificationAction(Context context);
+
+    interface SystemActionsHandler {
+        void executeAction(@TvPipAction.ActionType int actionType);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java
new file mode 100644
index 0000000..fa62a73
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE;
+import static com.android.wm.shell.pip.tv.TvPipController.ACTION_CLOSE_PIP;
+import static com.android.wm.shell.pip.tv.TvPipController.ACTION_MOVE_PIP;
+import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TOGGLE_EXPANDED_PIP;
+import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TO_FULLSCREEN;
+
+import android.annotation.NonNull;
+import android.app.RemoteAction;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip.PipMediaController;
+import com.android.wm.shell.pip.PipUtils;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Creates the system TvPipActions (fullscreen, close, move, expand/collapse),  and handles all the
+ * changes to the actions, including the custom app actions and media actions. Other components can
+ * listen to those changes.
+ */
+public class TvPipActionsProvider implements TvPipAction.SystemActionsHandler {
+    private static final String TAG = TvPipActionsProvider.class.getSimpleName();
+
+    private static final int CLOSE_ACTION_INDEX = 1;
+    private static final int FIRST_CUSTOM_ACTION_INDEX = 2;
+
+    private final List<Listener> mListeners = new ArrayList<>();
+    private final TvPipAction.SystemActionsHandler mSystemActionsHandler;
+
+    private final List<TvPipAction> mActionsList;
+    private final TvPipSystemAction mDefaultCloseAction;
+    private final TvPipSystemAction mExpandCollapseAction;
+
+    private final List<RemoteAction> mMediaActions = new ArrayList<>();
+    private final List<RemoteAction> mAppActions = new ArrayList<>();
+
+    public TvPipActionsProvider(Context context, PipMediaController pipMediaController,
+            TvPipAction.SystemActionsHandler systemActionsHandler) {
+        mSystemActionsHandler = systemActionsHandler;
+
+        mActionsList = new ArrayList<>();
+        mActionsList.add(new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen,
+                R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context,
+                mSystemActionsHandler));
+
+        mDefaultCloseAction = new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close,
+                R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context, mSystemActionsHandler);
+        mActionsList.add(mDefaultCloseAction);
+
+        mActionsList.add(new TvPipSystemAction(ACTION_MOVE, R.string.pip_move,
+                R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context, mSystemActionsHandler));
+
+        mExpandCollapseAction = new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse,
+                R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context,
+                mSystemActionsHandler);
+        mActionsList.add(mExpandCollapseAction);
+
+        pipMediaController.addActionListener(this::onMediaActionsChanged);
+    }
+
+    @Override
+    public void executeAction(@TvPipAction.ActionType int actionType) {
+        if (mSystemActionsHandler != null) {
+            mSystemActionsHandler.executeAction(actionType);
+        }
+    }
+
+    private void notifyActionsChanged(int added, int changed, int startIndex) {
+        for (Listener listener : mListeners) {
+            listener.onActionsChanged(added, changed, startIndex);
+        }
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    public void setAppActions(@NonNull List<RemoteAction> appActions, RemoteAction closeAction) {
+        // Update close action.
+        mActionsList.set(CLOSE_ACTION_INDEX,
+                closeAction == null ? mDefaultCloseAction
+                        : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction,
+                                mSystemActionsHandler));
+        notifyActionsChanged(/* added= */ 0, /* updated= */ 1, CLOSE_ACTION_INDEX);
+
+        // Replace custom actions with new ones.
+        mAppActions.clear();
+        for (RemoteAction action : appActions) {
+            if (action != null && !PipUtils.remoteActionsMatch(action, closeAction)) {
+                // Only show actions that aren't duplicates of the custom close action.
+                mAppActions.add(action);
+            }
+        }
+
+        updateCustomActions(mAppActions);
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public void onMediaActionsChanged(List<RemoteAction> actions) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: onMediaActionsChanged()", TAG);
+
+        mMediaActions.clear();
+        // Don't show disabled actions.
+        for (RemoteAction remoteAction : actions) {
+            if (remoteAction.isEnabled()) {
+                mMediaActions.add(remoteAction);
+            }
+        }
+
+        updateCustomActions(mMediaActions);
+    }
+
+    private void updateCustomActions(@NonNull List<RemoteAction> customActions) {
+        List<RemoteAction> newCustomActions = customActions;
+        if (newCustomActions == mMediaActions && !mAppActions.isEmpty()) {
+            // Don't show the media actions while there are app actions.
+            return;
+        } else if (newCustomActions == mAppActions && mAppActions.isEmpty()) {
+            // If all the app actions were removed, show the media actions.
+            newCustomActions = mMediaActions;
+        }
+
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: replaceCustomActions, count: %d", TAG, newCustomActions.size());
+        int oldCustomActionsCount = 0;
+        for (TvPipAction action : mActionsList) {
+            if (action.getActionType() == ACTION_CUSTOM) {
+                oldCustomActionsCount++;
+            }
+        }
+        mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM);
+
+        List<TvPipAction> actions = new ArrayList<>();
+        for (RemoteAction action : newCustomActions) {
+            actions.add(new TvPipCustomAction(ACTION_CUSTOM, action, mSystemActionsHandler));
+        }
+        mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, actions);
+
+        int added = newCustomActions.size() - oldCustomActionsCount;
+        int changed = Math.min(newCustomActions.size(), oldCustomActionsCount);
+        notifyActionsChanged(added, changed, FIRST_CUSTOM_ACTION_INDEX);
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    public void updateExpansionEnabled(boolean enabled) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: updateExpansionState, enabled: %b", TAG, enabled);
+        int actionIndex = mActionsList.indexOf(mExpandCollapseAction);
+        boolean actionInList = actionIndex != -1;
+        if (enabled && !actionInList) {
+            mActionsList.add(mExpandCollapseAction);
+            actionIndex = mActionsList.size() - 1;
+        } else if (!enabled && actionInList) {
+            mActionsList.remove(actionIndex);
+        } else {
+            return;
+        }
+        notifyActionsChanged(/* added= */ enabled ? 1 : -1, /* updated= */ 0, actionIndex);
+    }
+
+    @VisibleForTesting(visibility = PACKAGE)
+    public void onPipExpansionToggled(boolean expanded) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: onPipExpansionToggled, expanded: %b", TAG, expanded);
+
+        mExpandCollapseAction.update(
+                expanded ? R.string.pip_collapse : R.string.pip_expand,
+                expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand);
+
+        notifyActionsChanged(/* added= */ 0, /* updated= */ 1,
+                mActionsList.indexOf(mExpandCollapseAction));
+    }
+
+    List<TvPipAction> getActionsList() {
+        return mActionsList;
+    }
+
+    @NonNull
+    TvPipAction getCloseAction() {
+        return mActionsList.get(CLOSE_ACTION_INDEX);
+    }
+
+    void addListener(Listener listener) {
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener);
+        }
+    }
+
+    /**
+     * Returns the index of the first action of the given action type or -1 if none can be found.
+     */
+    int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) {
+        for (int i = 0; i < mActionsList.size(); i++) {
+            if (mActionsList.get(i).getActionType() == actionType) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Allow components to listen to updates to the actions list, including where they happen so
+     * that changes can be animated.
+     */
+    interface Listener {
+        /**
+         * Notifies the listener how many actions were added/removed or updated.
+         *
+         * @param added      can be positive (number of actions added), negative (number of actions
+         *                   removed) or zero (the number of actions stayed the same).
+         * @param updated    the number of actions that might have been updated and need to be
+         *                   refreshed.
+         * @param startIndex The index of the first updated action. The added/removed actions start
+         *                   at (startIndex + updated).
+         */
+        void onActionsChanged(int added, int updated, int startIndex);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 3e8de45..7671081 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -22,13 +22,16 @@
 import android.annotation.IntDef;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
-import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.app.TaskInfo;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.view.Gravity;
 
@@ -67,8 +70,8 @@
  */
 public class TvPipController implements PipTransitionController.PipTransitionCallback,
         TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
-        TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener,
-        ConfigurationChangeListener, UserChangeListener {
+        DisplayController.OnDisplaysChangedListener, ConfigurationChangeListener,
+        UserChangeListener {
     private static final String TAG = "TvPipController";
 
     private static final int NONEXISTENT_TASK_ID = -1;
@@ -98,6 +101,17 @@
      */
     private static final int STATE_PIP_MENU = 2;
 
+    static final String ACTION_SHOW_PIP_MENU =
+            "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
+    static final String ACTION_CLOSE_PIP =
+            "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP";
+    static final String ACTION_MOVE_PIP =
+            "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
+    static final String ACTION_TOGGLE_EXPANDED_PIP =
+            "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP";
+    static final String ACTION_TO_FULLSCREEN =
+            "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN";
+
     private final Context mContext;
 
     private final ShellController mShellController;
@@ -107,6 +121,7 @@
     private final PipAppOpsListener mAppOpsListener;
     private final PipTaskOrganizer mPipTaskOrganizer;
     private final PipMediaController mPipMediaController;
+    private final TvPipActionsProvider mTvPipActionsProvider;
     private final TvPipNotificationController mPipNotificationController;
     private final TvPipMenuController mTvPipMenuController;
     private final PipTransitionController mPipTransitionController;
@@ -115,14 +130,16 @@
     private final DisplayController mDisplayController;
     private final WindowManagerShellWrapper mWmShellWrapper;
     private final ShellExecutor mMainExecutor;
+    private final Handler mMainHandler; // For registering the broadcast receiver
     private final TvPipImpl mImpl = new TvPipImpl();
 
+    private final ActionBroadcastReceiver mActionBroadcastReceiver;
+
     @State
     private int mState = STATE_NO_PIP;
     private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
     private int mPinnedTaskId = NONEXISTENT_TASK_ID;
 
-    private RemoteAction mCloseAction;
     // How long the shell will wait for the app to close the PiP if a custom action is set.
     private int mPipForceCloseDelay;
 
@@ -146,6 +163,7 @@
             PipParamsChangedForwarder pipParamsChangedForwarder,
             DisplayController displayController,
             WindowManagerShellWrapper wmShell,
+            Handler mainHandler,
             ShellExecutor mainExecutor) {
         return new TvPipController(
                 context,
@@ -164,6 +182,7 @@
                 pipParamsChangedForwarder,
                 displayController,
                 wmShell,
+                mainHandler,
                 mainExecutor).mImpl;
     }
 
@@ -184,8 +203,10 @@
             PipParamsChangedForwarder pipParamsChangedForwarder,
             DisplayController displayController,
             WindowManagerShellWrapper wmShellWrapper,
+            Handler mainHandler,
             ShellExecutor mainExecutor) {
         mContext = context;
+        mMainHandler = mainHandler;
         mMainExecutor = mainExecutor;
         mShellController = shellController;
         mDisplayController = displayController;
@@ -198,12 +219,17 @@
         mTvPipBoundsController.setListener(this);
 
         mPipMediaController = pipMediaController;
+        mTvPipActionsProvider = new TvPipActionsProvider(context, pipMediaController,
+                this::executeAction);
 
         mPipNotificationController = pipNotificationController;
-        mPipNotificationController.setDelegate(this);
+        mPipNotificationController.setTvPipActionsProvider(mTvPipActionsProvider);
 
         mTvPipMenuController = tvPipMenuController;
         mTvPipMenuController.setDelegate(this);
+        mTvPipMenuController.setTvPipActionsProvider(mTvPipActionsProvider);
+
+        mActionBroadcastReceiver = new ActionBroadcastReceiver();
 
         mAppOpsListener = pipAppOpsListener;
         mPipTaskOrganizer = pipTaskOrganizer;
@@ -241,7 +267,7 @@
                 "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
 
         loadConfigurations();
-        mPipNotificationController.onConfigurationChanged(mContext);
+        mPipNotificationController.onConfigurationChanged();
         mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
     }
 
@@ -256,9 +282,10 @@
      * Starts the process if bringing up the Pip menu if by issuing a command to move Pip
      * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip
      * task/window is properly positioned in {@link #onPipTransitionFinished(int)}.
+     *
+     * @param moveMenu If true, show the moveMenu, otherwise show the regular menu.
      */
-    @Override
-    public void showPictureInPictureMenu() {
+    private void showPictureInPictureMenu(boolean moveMenu) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState));
 
@@ -269,7 +296,11 @@
         }
 
         setState(STATE_PIP_MENU);
-        mTvPipMenuController.showMenu();
+        if (moveMenu) {
+            mTvPipMenuController.showMovementMenu();
+        } else {
+            mTvPipMenuController.showMenu();
+        }
         updatePinnedStackBounds();
     }
 
@@ -289,8 +320,7 @@
     /**
      * Opens the "Pip-ed" Activity fullscreen.
      */
-    @Override
-    public void movePipToFullscreen() {
+    private void movePipToFullscreen() {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState));
 
@@ -298,8 +328,7 @@
         onPipDisappeared();
     }
 
-    @Override
-    public void togglePipExpansion() {
+    private void togglePipExpansion() {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: togglePipExpansion()", TAG);
         boolean expanding = !mTvPipBoundsState.isTvPipExpanded();
@@ -310,18 +339,11 @@
         }
         mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
         mTvPipBoundsState.setTvPipExpanded(expanding);
-        mPipNotificationController.updateExpansionState();
 
         updatePinnedStackBounds();
     }
 
     @Override
-    public void enterPipMovementMenu() {
-        setState(STATE_PIP_MENU);
-        mTvPipMenuController.showMovementMenuOnly();
-    }
-
-    @Override
     public void movePip(int keycode) {
         if (mTvPipBoundsAlgorithm.updateGravity(keycode)) {
             mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity());
@@ -373,30 +395,23 @@
     @Override
     public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
         mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
-                animationDuration, rect -> mTvPipMenuController.updateExpansionState());
+                animationDuration, null);
         mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
     }
 
     /**
      * Closes Pip window.
      */
-    @Override
     public void closePip() {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState),
-                mCloseAction);
+        closeCurrentPiP(mPinnedTaskId);
+    }
 
-        if (mCloseAction != null) {
-            try {
-                mCloseAction.getActionIntent().send();
-            } catch (PendingIntent.CanceledException e) {
-                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                        "%s: Failed to send close action, %s", TAG, e);
-            }
-            mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay);
-        } else {
-            closeCurrentPiP(mPinnedTaskId);
-        }
+    /**
+     * Force close the current PiP after some time in case the custom action hasn't done it by
+     * itself.
+     */
+    public void customClosePip() {
+        mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay);
     }
 
     private void closeCurrentPiP(int pinnedTaskId) {
@@ -426,6 +441,7 @@
         mPinnedTaskId = pinnedTask.taskId;
 
         mPipMediaController.onActivityPinned();
+        mActionBroadcastReceiver.register();
         mPipNotificationController.show(pinnedTask.topActivity.getPackageName());
     }
 
@@ -445,6 +461,8 @@
                 "%s: onPipDisappeared() state=%s", TAG, stateToName(mState));
 
         mPipNotificationController.dismiss();
+        mActionBroadcastReceiver.unregister();
+
         mTvPipMenuController.closeMenu();
         mTvPipBoundsState.resetTvPipState();
         mTvPipBoundsController.onPipDismissed();
@@ -454,6 +472,11 @@
 
     @Override
     public void onPipTransitionStarted(int direction, Rect currentPipBounds) {
+        final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction);
+        if (enterPipTransition && mState == STATE_NO_PIP) {
+            // Set the initial ability to expand the PiP when entering PiP.
+            updateExpansionState();
+        }
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onPipTransition_Started(), state=%s, direction=%d",
                 TAG, stateToName(mState), direction);
@@ -465,6 +488,7 @@
                 "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
         mTvPipMenuController.onPipTransitionFinished(
                 PipAnimationController.isInPipDirection(direction));
+        mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded());
     }
 
     @Override
@@ -477,6 +501,12 @@
                 "%s: onPipTransition_Finished(), state=%s, direction=%d",
                 TAG, stateToName(mState), direction);
         mTvPipMenuController.onPipTransitionFinished(enterPipTransition);
+        mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded());
+    }
+
+    private void updateExpansionState() {
+        mTvPipActionsProvider.updateExpansionEnabled(mTvPipBoundsState.isTvExpandedPipSupported()
+                && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0);
     }
 
     private void setState(@State int state) {
@@ -534,8 +564,7 @@
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                         "%s: onActionsChanged()", TAG);
 
-                mTvPipMenuController.setAppActions(actions, closeAction);
-                mCloseAction = closeAction;
+                mTvPipActionsProvider.setAppActions(actions, closeAction);
             }
 
             @Override
@@ -555,7 +584,7 @@
                         "%s: onExpandedAspectRatioChanged: %f", TAG, ratio);
 
                 mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false);
-                mTvPipMenuController.updateExpansionState();
+                updateExpansionState();
 
                 // 1) PiP is expanded and only aspect ratio changed, but wasn't disabled
                 // --> update bounds, but don't toggle
@@ -662,6 +691,90 @@
         }
     }
 
+    private void executeAction(@TvPipAction.ActionType int actionType) {
+        switch (actionType) {
+            case TvPipAction.ACTION_FULLSCREEN:
+                movePipToFullscreen();
+                break;
+            case TvPipAction.ACTION_CLOSE:
+                closePip();
+                break;
+            case TvPipAction.ACTION_MOVE:
+                showPictureInPictureMenu(/* moveMenu= */ true);
+                break;
+            case TvPipAction.ACTION_CUSTOM_CLOSE:
+                customClosePip();
+                break;
+            case TvPipAction.ACTION_EXPAND_COLLAPSE:
+                togglePipExpansion();
+                break;
+            default:
+                // NOOP
+                break;
+        }
+    }
+
+    private class ActionBroadcastReceiver extends BroadcastReceiver {
+        private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
+
+        final IntentFilter mIntentFilter;
+
+        {
+            mIntentFilter = new IntentFilter();
+            mIntentFilter.addAction(ACTION_CLOSE_PIP);
+            mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
+            mIntentFilter.addAction(ACTION_MOVE_PIP);
+            mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP);
+            mIntentFilter.addAction(ACTION_TO_FULLSCREEN);
+        }
+
+        boolean mRegistered = false;
+
+        void register() {
+            if (mRegistered) return;
+
+            mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION,
+                    mMainHandler);
+            mRegistered = true;
+        }
+
+        void unregister() {
+            if (!mRegistered) return;
+
+            mContext.unregisterReceiver(this);
+            mRegistered = false;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: on(Broadcast)Receive(), action=%s", TAG, action);
+
+            if (ACTION_SHOW_PIP_MENU.equals(action)) {
+                showPictureInPictureMenu(/* moveMenu= */ false);
+            } else {
+                executeAction(getCorrespondingActionType(action));
+            }
+        }
+
+        @TvPipAction.ActionType
+        private int getCorrespondingActionType(String broadcast) {
+            if (ACTION_CLOSE_PIP.equals(broadcast)) {
+                return TvPipAction.ACTION_CLOSE;
+            } else if (ACTION_MOVE_PIP.equals(broadcast)) {
+                return TvPipAction.ACTION_MOVE;
+            } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(broadcast)) {
+                return TvPipAction.ACTION_EXPAND_COLLAPSE;
+            } else if (ACTION_TO_FULLSCREEN.equals(broadcast)) {
+                return TvPipAction.ACTION_FULLSCREEN;
+            }
+
+            // Default: handle it like an action we don't know the content of.
+            return TvPipAction.ACTION_CUSTOM;
+        }
+    }
+
     private class TvPipImpl implements Pip {
         // Not used
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java
new file mode 100644
index 0000000..449a2bf
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE;
+import static android.app.Notification.Action.SEMANTIC_ACTION_NONE;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.TvWindowMenuActionButton;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A TvPipAction for actions that the app provides via {@link
+ * android.app.PictureInPictureParams.Builder#setCloseAction(RemoteAction)} or {@link
+ * android.app.PictureInPictureParams.Builder#setActions(List)}.
+ */
+public class TvPipCustomAction extends TvPipAction {
+    private static final String TAG = TvPipCustomAction.class.getSimpleName();
+
+    private final RemoteAction mRemoteAction;
+
+    TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction,
+            SystemActionsHandler systemActionsHandler) {
+        super(actionType, systemActionsHandler);
+        Objects.requireNonNull(remoteAction);
+        mRemoteAction = remoteAction;
+    }
+
+    void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) {
+        if (button == null || mainHandler == null) return;
+        if (mRemoteAction.getContentDescription().length() > 0) {
+            button.setTextAndDescription(mRemoteAction.getContentDescription());
+        } else {
+            button.setTextAndDescription(mRemoteAction.getTitle());
+        }
+        button.setImageIconAsync(mRemoteAction.getIcon(), mainHandler);
+        button.setEnabled(isCloseAction() || mRemoteAction.isEnabled());
+    }
+
+    PendingIntent getPendingIntent() {
+        return mRemoteAction.getActionIntent();
+    }
+
+    void executeAction() {
+        super.executeAction();
+        try {
+            mRemoteAction.getActionIntent().send();
+        } catch (PendingIntent.CanceledException e) {
+            ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Failed to send action, %s", TAG, e);
+        }
+    }
+
+    @Override
+    Notification.Action toNotificationAction(Context context) {
+        Notification.Action.Builder builder = new Notification.Action.Builder(
+                mRemoteAction.getIcon(),
+                mRemoteAction.getTitle(),
+                mRemoteAction.getActionIntent());
+        Bundle extras = new Bundle();
+        extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION,
+                mRemoteAction.getContentDescription());
+        builder.addExtras(extras);
+
+        builder.setSemanticAction(isCloseAction()
+                ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE);
+        builder.setContextual(true);
+        return builder.build();
+    }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index ab7edbf..00e4f47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -41,13 +41,10 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.SystemWindows;
-import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipMenuController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 
 /**
  * Manages the visibility of the PiP Menu as user interacts with PiP.
@@ -60,22 +57,20 @@
     private final SystemWindows mSystemWindows;
     private final TvPipBoundsState mTvPipBoundsState;
     private final Handler mMainHandler;
+    private TvPipActionsProvider mTvPipActionsProvider;
 
     private Delegate mDelegate;
     private SurfaceControl mLeash;
     private TvPipMenuView mPipMenuView;
     private View mPipBackgroundView;
 
+    private boolean mMenuIsOpen;
     // User can actively move the PiP via the DPAD.
     private boolean mInMoveMode;
     // Used when only showing the move menu since we want to close the menu completely when
     // exiting the move menu instead of showing the regular button menu.
     private boolean mCloseAfterExitMoveMenu;
 
-    private final List<RemoteAction> mMediaActions = new ArrayList<>();
-    private final List<RemoteAction> mAppActions = new ArrayList<>();
-    private RemoteAction mCloseAction;
-
     private SyncRtSurfaceTransactionApplier mApplier;
     private SyncRtSurfaceTransactionApplier mBackgroundApplier;
     RectF mTmpSourceRectF = new RectF();
@@ -83,8 +78,7 @@
     Matrix mMoveTransform = new Matrix();
 
     public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
-            SystemWindows systemWindows, PipMediaController pipMediaController,
-            Handler mainHandler) {
+            SystemWindows systemWindows, Handler mainHandler) {
         mContext = context;
         mTvPipBoundsState = tvPipBoundsState;
         mSystemWindows = systemWindows;
@@ -101,9 +95,6 @@
         context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver,
                 new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */,
                 mainHandler, Context.RECEIVER_EXPORTED);
-
-        pipMediaController.addActionListener(this::onMediaActionsChanged);
-
     }
 
     void setDelegate(Delegate delegate) {
@@ -120,6 +111,10 @@
         mDelegate = delegate;
     }
 
+    void setTvPipActionsProvider(TvPipActionsProvider tvPipActionsProvider) {
+        mTvPipActionsProvider = tvPipActionsProvider;
+    }
+
     @Override
     public void attach(SurfaceControl leash) {
         if (mDelegate == null) {
@@ -146,15 +141,19 @@
         int pipMenuBorderWidth = mContext.getResources()
                 .getDimensionPixelSize(R.dimen.pip_menu_border_width);
         mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
-                    -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
+                -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
         mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
     }
 
     private void attachPipMenuView() {
-        mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this);
+        if (mTvPipActionsProvider == null) {
+            ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Actions provider is not set", TAG);
+            return;
+        }
+        mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider);
         setUpViewSurfaceZOrder(mPipMenuView, 1);
         addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
-        maybeUpdateMenuViewActions();
     }
 
     private void attachPipBackgroundView() {
@@ -189,17 +188,22 @@
         // and the menu view has been fully remeasured and relaid out, we add a small delay here by
         // posting on the handler.
         mMainHandler.post(() -> {
-            mPipMenuView.onPipTransitionFinished(
-                    enterTransition, mTvPipBoundsState.isTvPipExpanded());
+            if (mPipMenuView != null) {
+                mPipMenuView.onPipTransitionFinished(enterTransition);
+            }
         });
     }
 
-    void showMovementMenuOnly() {
+    void showMovementMenu() {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showMovementMenuOnly()", TAG);
         setInMoveMode(true);
-        mCloseAfterExitMoveMenu = true;
-        showMenuInternal();
+        if (mMenuIsOpen) {
+            mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
+        } else {
+            mCloseAfterExitMoveMenu = true;
+            showMenuInternal();
+        }
     }
 
     @Override
@@ -214,14 +218,13 @@
         if (mPipMenuView == null) {
             return;
         }
-        maybeUpdateMenuViewActions();
-        updateExpansionState();
 
+        mMenuIsOpen = true;
         grantPipMenuFocus(true);
         if (mInMoveMode) {
             mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
         } else {
-            mPipMenuView.showButtonsMenu();
+            mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false);
         }
         mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
     }
@@ -236,11 +239,6 @@
         mPipMenuView.showMovementHints(gravity);
     }
 
-    void updateExpansionState() {
-        mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported()
-                && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0);
-    }
-
     private Rect calculateMenuSurfaceBounds(Rect pipBounds) {
         return mPipMenuView.getPipMenuContainerBounds(pipBounds);
     }
@@ -252,6 +250,8 @@
         if (mPipMenuView == null) {
             return;
         }
+
+        mMenuIsOpen = false;
         mPipMenuView.hideAllUserControls();
         grantPipMenuFocus(false);
         mDelegate.onMenuClosed();
@@ -272,29 +272,19 @@
     }
 
     @Override
-    public void onEnterMoveMode() {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
-                mCloseAfterExitMoveMenu);
-        setInMoveMode(true);
-        mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
-    }
-
-    @Override
     public boolean onExitMoveMode() {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onExitMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
-                mCloseAfterExitMoveMenu);
+                "%s: onExitMoveMode - %b, close when exiting move menu: %b",
+                TAG, mInMoveMode, mCloseAfterExitMoveMenu);
 
-        if (mCloseAfterExitMoveMenu) {
-            setInMoveMode(false);
-            mCloseAfterExitMoveMenu = false;
-            closeMenu();
-            return true;
-        }
         if (mInMoveMode) {
             setInMoveMode(false);
-            mPipMenuView.showButtonsMenu();
+            if (mCloseAfterExitMoveMenu) {
+                mCloseAfterExitMoveMenu = false;
+                closeMenu();
+            } else {
+                mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true);
+            }
             return true;
         }
         return false;
@@ -319,51 +309,7 @@
 
     @Override
     public void setAppActions(List<RemoteAction> actions, RemoteAction closeAction) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: setAppActions()", TAG);
-        updateAdditionalActionsList(mAppActions, actions, closeAction);
-    }
-
-    private void onMediaActionsChanged(List<RemoteAction> actions) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onMediaActionsChanged()", TAG);
-
-        // Hide disabled actions.
-        List<RemoteAction> enabledActions = new ArrayList<>();
-        for (RemoteAction remoteAction : actions) {
-            if (remoteAction.isEnabled()) {
-                enabledActions.add(remoteAction);
-            }
-        }
-        updateAdditionalActionsList(mMediaActions, enabledActions, mCloseAction);
-    }
-
-    private void updateAdditionalActionsList(List<RemoteAction> destination,
-            @Nullable List<RemoteAction> source, RemoteAction closeAction) {
-        final int number = source != null ? source.size() : 0;
-        if (number == 0 && destination.isEmpty() && Objects.equals(closeAction, mCloseAction)) {
-            // Nothing changed.
-            return;
-        }
-
-        mCloseAction = closeAction;
-
-        destination.clear();
-        if (number > 0) {
-            destination.addAll(source);
-        }
-        maybeUpdateMenuViewActions();
-    }
-
-    private void maybeUpdateMenuViewActions() {
-        if (mPipMenuView == null) {
-            return;
-        }
-        if (!mAppActions.isEmpty()) {
-            mPipMenuView.setAdditionalActions(mAppActions, mCloseAction, mMainHandler);
-        } else {
-            mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler);
-        }
+        // NOOP - handled via the TvPipActionsProvider
     }
 
     @Override
@@ -544,42 +490,21 @@
     }
 
     @Override
-    public void onCloseButtonClick() {
-        mDelegate.closePip();
-    }
-
-    @Override
-    public void onFullscreenButtonClick() {
-        mDelegate.movePipToFullscreen();
-    }
-
-    @Override
-    public void onToggleExpandedMode() {
-        mDelegate.togglePipExpansion();
-    }
-
-    @Override
     public void onCloseEduText() {
         mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
         mDelegate.closeEduText();
     }
 
     interface Delegate {
-        void movePipToFullscreen();
-
         void movePip(int keycode);
 
         void onInMoveModeChanged();
 
         int getPipGravity();
 
-        void togglePipExpansion();
-
         void onMenuClosed();
 
         void closeEduText();
-
-        void closePip();
     }
 
     private void grantPipMenuFocus(boolean grantFocus) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 57e95c4..56c602a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -25,119 +25,102 @@
 import static android.view.KeyEvent.KEYCODE_DPAD_UP;
 import static android.view.KeyEvent.KEYCODE_ENTER;
 
-import android.app.PendingIntent;
-import android.app.RemoteAction;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE;
+
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Handler;
 import android.view.Gravity;
 import android.view.KeyEvent;
-import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewRootImpl;
 import android.view.accessibility.AccessibilityManager;
 import android.widget.FrameLayout;
-import android.widget.HorizontalScrollView;
 import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.widget.LinearLayoutManager;
+import com.android.internal.widget.RecyclerView;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.TvWindowMenuActionButton;
 import com.android.wm.shell.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
- * A View that represents Pip Menu on TV. It's responsible for displaying 3 ever-present Pip Menu
- * actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set
- * via a {@link #setAdditionalActions(List, RemoteAction, Handler)} call.
+ * A View that represents Pip Menu on TV. It's responsible for displaying the Pip menu actions from
+ * the TvPipActionsProvider as well as the buttons for manually moving the PiP.
  */
-public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
+public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.Listener {
     private static final String TAG = "TvPipMenuView";
 
-    private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
+    private final TvPipMenuView.Listener mListener;
 
-    private final Listener mListener;
+    private final TvPipActionsProvider mTvPipActionsProvider;
 
-    private final LinearLayout mActionButtonsContainer;
-    private final View mMenuFrameView;
-    private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
+    private final RecyclerView mActionButtonsRecyclerView;
+    private final LinearLayoutManager mButtonLayoutManager;
+    private final RecyclerViewAdapter mRecyclerViewAdapter;
+
     private final View mPipFrameView;
+    private final View mMenuFrameView;
     private final View mPipView;
+
+    private final View mPipBackground;
+    private final View mDimLayer;
+
     private final TvPipMenuEduTextDrawer mEduTextDrawer;
+
     private final int mPipMenuOuterSpace;
     private final int mPipMenuBorderWidth;
 
+    private final int mPipMenuFadeAnimationDuration;
+    private final int mResizeAnimationDuration;
+
     private final ImageView mArrowUp;
     private final ImageView mArrowRight;
     private final ImageView mArrowDown;
     private final ImageView mArrowLeft;
     private final TvWindowMenuActionButton mA11yDoneButton;
 
-    private final View mPipBackground;
-    private final View mDimLayer;
-
-    private final ScrollView mScrollView;
-    private final HorizontalScrollView mHorizontalScrollView;
-    private View mFocusedButton;
-
     private Rect mCurrentPipBounds;
     private boolean mMoveMenuIsVisible;
     private boolean mButtonMenuIsVisible;
-
-    private final TvWindowMenuActionButton mExpandButton;
-    private final TvWindowMenuActionButton mCloseButton;
-
     private boolean mSwitchingOrientation;
 
-    private final int mPipMenuFadeAnimationDuration;
-    private final int mResizeAnimationDuration;
-
     private final AccessibilityManager mA11yManager;
     private final Handler mMainHandler;
 
     public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler,
-            @NonNull Listener listener) {
+            @NonNull Listener listener, TvPipActionsProvider tvPipActionsProvider) {
         super(context, null, 0, 0);
-
         inflate(context, R.layout.tv_pip_menu, this);
 
         mMainHandler = mainHandler;
         mListener = listener;
-
         mA11yManager = context.getSystemService(AccessibilityManager.class);
 
-        mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
-        mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button)
-                .setOnClickListener(this);
+        mActionButtonsRecyclerView = findViewById(R.id.tv_pip_menu_action_buttons);
+        mButtonLayoutManager = new LinearLayoutManager(mContext);
+        mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager);
+        mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true);
 
-        mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button);
-        mCloseButton.setOnClickListener(this);
-        mCloseButton.setIsCustomCloseAction(true);
+        mTvPipActionsProvider = tvPipActionsProvider;
+        mRecyclerViewAdapter = new RecyclerViewAdapter(tvPipActionsProvider.getActionsList());
+        mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter);
 
-        mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button)
-                .setOnClickListener(this);
-        mExpandButton = findViewById(R.id.tv_pip_menu_expand_button);
-        mExpandButton.setOnClickListener(this);
-
-        mPipBackground = findViewById(R.id.tv_pip_menu_background);
-        mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer);
-
-        mScrollView = findViewById(R.id.tv_pip_menu_scroll);
-        mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll);
+        tvPipActionsProvider.addListener(this);
 
         mMenuFrameView = findViewById(R.id.tv_pip_menu_frame);
         mPipFrameView = findViewById(R.id.tv_pip_border);
         mPipView = findViewById(R.id.tv_pip);
 
+        mPipBackground = findViewById(R.id.tv_pip_menu_background);
+        mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer);
+
         mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up);
         mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right);
         mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down);
@@ -160,8 +143,12 @@
     }
 
     void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
+        if (targetBounds == null) {
+            return;
+        }
+
         // Fade out content by fading in view on top.
-        if (mCurrentPipBounds != null && targetBounds != null) {
+        if (mCurrentPipBounds != null) {
             boolean ratioChanged = PipUtils.aspectRatioChanged(
                     mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
                     targetBounds.width() / (float) targetBounds.height());
@@ -177,7 +164,7 @@
         // Update buttons.
         final boolean vertical = targetBounds.height() > targetBounds.width();
         final boolean orientationChanged =
-                vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
+                vertical != (mButtonLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL);
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
                 TAG, orientationChanged);
@@ -187,27 +174,27 @@
 
         if (mButtonMenuIsVisible) {
             mSwitchingOrientation = true;
-            mActionButtonsContainer.animate()
+            mActionButtonsRecyclerView.animate()
                     .alpha(0)
                     .setInterpolator(TvPipInterpolators.EXIT)
                     .setDuration(mResizeAnimationDuration / 2)
                     .withEndAction(() -> {
-                        changeButtonScrollOrientation(targetBounds);
-                        updateButtonGravity(targetBounds);
+                        mButtonLayoutManager.setOrientation(vertical
+                                ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL);
                         // Only make buttons visible again in onPipTransitionFinished to keep in
                         // sync with PiP content alpha animation.
                     });
         } else {
-            changeButtonScrollOrientation(targetBounds);
-            updateButtonGravity(targetBounds);
+            mButtonLayoutManager.setOrientation(vertical
+                    ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL);
         }
     }
 
-    void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) {
+    void onPipTransitionFinished(boolean enterTransition) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onPipTransitionFinished()", TAG);
 
-        // Fade in content by fading out view on top.
+        // Fade in content by fading out view on top (faded out at every aspect ratio change).
         mPipBackground.animate()
                 .alpha(0f)
                 .setDuration(mResizeAnimationDuration / 2)
@@ -218,16 +205,11 @@
             mEduTextDrawer.init();
         }
 
-        setIsExpanded(isTvPipExpanded);
-
-        // Update buttons.
         if (mSwitchingOrientation) {
-            mActionButtonsContainer.animate()
+            mActionButtonsRecyclerView.animate()
                     .alpha(1)
                     .setInterpolator(TvPipInterpolators.ENTER)
                     .setDuration(mResizeAnimationDuration / 2);
-        } else {
-            refocusPreviousButton();
         }
         mSwitchingOrientation = false;
     }
@@ -240,107 +222,9 @@
                 "%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(),
                 updatedBounds.height());
         mCurrentPipBounds = updatedBounds;
-        if (!mSwitchingOrientation) {
-            updateButtonGravity(mCurrentPipBounds);
-        }
-
         updatePipFrameBounds();
     }
 
-    private void changeButtonScrollOrientation(Rect bounds) {
-        final boolean vertical = bounds.height() > bounds.width();
-
-        final ViewGroup oldScrollView = vertical ? mHorizontalScrollView : mScrollView;
-        final ViewGroup newScrollView = vertical ? mScrollView : mHorizontalScrollView;
-
-        if (oldScrollView.getChildCount() == 1) {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: orientation changed", TAG);
-            oldScrollView.removeView(mActionButtonsContainer);
-            oldScrollView.setVisibility(GONE);
-            mActionButtonsContainer.setOrientation(vertical ? LinearLayout.VERTICAL
-                    : LinearLayout.HORIZONTAL);
-            newScrollView.addView(mActionButtonsContainer);
-            newScrollView.setVisibility(VISIBLE);
-            if (mFocusedButton != null) {
-                mFocusedButton.requestFocus();
-            }
-        }
-    }
-
-    /**
-     * Change button gravity based on new dimensions
-     */
-    private void updateButtonGravity(Rect bounds) {
-        final boolean vertical = bounds.height() > bounds.width();
-        // Use Math.max since the possible orientation change might not have been applied yet.
-        final int buttonsSize = Math.max(mActionButtonsContainer.getHeight(),
-                mActionButtonsContainer.getWidth());
-
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: buttons container width: %s, height: %s", TAG,
-                mActionButtonsContainer.getWidth(), mActionButtonsContainer.getHeight());
-
-        final boolean buttonsFit =
-                vertical ? buttonsSize < bounds.height()
-                        : buttonsSize < bounds.width();
-        final int buttonGravity = buttonsFit ? Gravity.CENTER
-                : (vertical ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL);
-
-        final LayoutParams params = (LayoutParams) mActionButtonsContainer.getLayoutParams();
-        params.gravity = buttonGravity;
-        mActionButtonsContainer.setLayoutParams(params);
-
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: vertical: %b, buttonsFit: %b, gravity: %s", TAG, vertical, buttonsFit,
-                Gravity.toString(buttonGravity));
-    }
-
-    private void refocusPreviousButton() {
-        if (mMoveMenuIsVisible || mCurrentPipBounds == null || mFocusedButton == null) {
-            return;
-        }
-        final boolean vertical = mCurrentPipBounds.height() > mCurrentPipBounds.width();
-
-        if (!mFocusedButton.hasFocus()) {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: request focus from: %s", TAG, mFocusedButton);
-            mFocusedButton.requestFocus();
-        } else {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: already focused: %s", TAG, mFocusedButton);
-        }
-
-        // Do we need to scroll?
-        final Rect buttonBounds = new Rect();
-        final Rect scrollBounds = new Rect();
-        if (vertical) {
-            mScrollView.getDrawingRect(scrollBounds);
-        } else {
-            mHorizontalScrollView.getDrawingRect(scrollBounds);
-        }
-        mFocusedButton.getHitRect(buttonBounds);
-
-        if (scrollBounds.contains(buttonBounds)) {
-            // Button is already completely visible, don't scroll
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: not scrolling", TAG);
-            return;
-        }
-
-        // Scrolling so the button is visible to the user.
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: scrolling to focused button", TAG);
-
-        if (vertical) {
-            mScrollView.smoothScrollTo((int) mFocusedButton.getX(),
-                    (int) mFocusedButton.getY());
-        } else {
-            mHorizontalScrollView.smoothScrollTo((int) mFocusedButton.getX(),
-                    (int) mFocusedButton.getY());
-        }
-    }
-
     Rect getPipMenuContainerBounds(Rect pipBounds) {
         final Rect menuUiBounds = new Rect(pipBounds);
         menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
@@ -370,20 +254,14 @@
             mPipView.setLayoutParams(pipViewParams);
         }
 
-
-    }
-
-    void setExpandedModeEnabled(boolean enabled) {
-        mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
-    }
-
-    void setIsExpanded(boolean expanded) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: setIsExpanded, expanded: %b", TAG, expanded);
-        mExpandButton.setImageResource(
-                expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand);
-        mExpandButton.setTextAndDescription(
-                expanded ? R.string.pip_collapse : R.string.pip_expand);
+        // Keep focused button within the visible area while the PiP is changing size. Otherwise,
+        // the button would lose focus which would cause a need for scrolling and re-focusing after
+        // the animation finishes, which does not look good.
+        View focusedChild = mActionButtonsRecyclerView.getFocusedChild();
+        if (focusedChild != null) {
+            mActionButtonsRecyclerView.scrollToPosition(
+                    mActionButtonsRecyclerView.getChildLayoutPosition(focusedChild));
+        }
     }
 
     /**
@@ -391,48 +269,63 @@
      */
     void showMoveMenu(int gravity) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
-        showButtonsMenu(false);
         showMovementHints(gravity);
+        setMenuButtonsVisible(false);
         setFrameHighlighted(true);
 
-        mHorizontalScrollView.setFocusable(false);
-        mScrollView.setFocusable(false);
+        animateAlphaTo(mA11yManager.isEnabled() ? 1f : 0f, mDimLayer);
 
         mEduTextDrawer.closeIfNeeded();
     }
 
-    void showButtonsMenu() {
+
+    void showButtonsMenu(boolean exitingMoveMode) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: showButtonsMenu()", TAG);
-        showButtonsMenu(true);
+                "%s: showButtonsMenu(), exitingMoveMode %b", TAG, exitingMoveMode);
+        setMenuButtonsVisible(true);
         hideMovementHints();
         setFrameHighlighted(true);
+        animateAlphaTo(1f, mDimLayer);
+        mEduTextDrawer.closeIfNeeded();
 
-        mHorizontalScrollView.setFocusable(true);
-        mScrollView.setFocusable(true);
-
-        // Always focus on the first button when opening the menu, except directly after moving.
-        if (mFocusedButton == null) {
-            // Focus on first button (there is a Space at position 0)
-            mFocusedButton = mActionButtonsContainer.getChildAt(1);
-            // Reset scroll position.
-            mScrollView.scrollTo(0, 0);
-            mHorizontalScrollView.scrollTo(
-                    isLayoutRtl() ? mActionButtonsContainer.getWidth() : 0, 0);
+        if (exitingMoveMode) {
+            scrollAndRefocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE),
+                    /* alwaysScroll= */ false);
+        } else {
+            scrollAndRefocusButton(0, /* alwaysScroll= */ true);
         }
-        refocusPreviousButton();
+    }
+
+    private void scrollAndRefocusButton(int position, boolean alwaysScroll) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: scrollAndRefocusButton, target: %d", TAG, position);
+
+        if (alwaysScroll || !refocusButton(position)) {
+            mButtonLayoutManager.scrollToPositionWithOffset(position, 0);
+            mActionButtonsRecyclerView.post(() -> refocusButton(position));
+        }
     }
 
     /**
-     * Hides all menu views, including the menu frame.
+     * @return true if focus was requested, false if focus request could not be carried out due to
+     * the view for the position not being available (scrolling beforehand will be necessary).
      */
+    private boolean refocusButton(int position) {
+        View itemToFocus = mButtonLayoutManager.findViewByPosition(position);
+        if (itemToFocus != null) {
+            itemToFocus.requestFocus();
+            itemToFocus.requestAccessibilityFocus();
+        }
+        return itemToFocus != null;
+    }
+
     void hideAllUserControls() {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: hideAllUserControls()", TAG);
-        mFocusedButton = null;
-        showButtonsMenu(false);
+        setMenuButtonsVisible(false);
         hideMovementHints();
         setFrameHighlighted(false);
+        animateAlphaTo(0f, mDimLayer);
     }
 
     @Override
@@ -463,134 +356,19 @@
                 });
     }
 
-    /**
-     * Button order:
-     * - Fullscreen
-     * - Close
-     * - Custom actions (app or media actions)
-     * - System actions
-     */
-    void setAdditionalActions(List<RemoteAction> actions, RemoteAction closeAction,
-            Handler mainHandler) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: setAdditionalActions()", TAG);
-
-        // Replace system close action with custom close action if available
-        if (closeAction != null) {
-            setActionForButton(closeAction, mCloseButton, mainHandler);
-        } else {
-            mCloseButton.setTextAndDescription(R.string.pip_close);
-            mCloseButton.setImageResource(R.drawable.pip_ic_close_white);
-        }
-        mCloseButton.setIsCustomCloseAction(closeAction != null);
-        // Make sure the close action is always enabled
-        mCloseButton.setEnabled(true);
-
-        // Make sure we exactly as many additional buttons as we have actions to display.
-        final int actionsNumber = actions.size();
-        int buttonsNumber = mAdditionalButtons.size();
-        if (actionsNumber > buttonsNumber) {
-            // Add buttons until we have enough to display all the actions.
-            while (actionsNumber > buttonsNumber) {
-                TvWindowMenuActionButton button = new TvWindowMenuActionButton(mContext);
-                button.setOnClickListener(this);
-
-                mActionButtonsContainer.addView(button,
-                        FIRST_CUSTOM_ACTION_POSITION + buttonsNumber);
-                mAdditionalButtons.add(button);
-
-                buttonsNumber++;
-            }
-        } else if (actionsNumber < buttonsNumber) {
-            // Hide buttons until we as many as the actions.
-            while (actionsNumber < buttonsNumber) {
-                final View button = mAdditionalButtons.get(buttonsNumber - 1);
-                button.setVisibility(View.GONE);
-                button.setTag(null);
-
-                buttonsNumber--;
-            }
-        }
-
-        // "Assign" actions to the buttons.
-        for (int index = 0; index < actionsNumber; index++) {
-            final RemoteAction action = actions.get(index);
-            final TvWindowMenuActionButton button = mAdditionalButtons.get(index);
-
-            // Remove action if it matches the custom close action.
-            if (PipUtils.remoteActionsMatch(action, closeAction)) {
-                button.setVisibility(GONE);
-                continue;
-            }
-            setActionForButton(action, button, mainHandler);
-        }
-
-        if (mCurrentPipBounds != null) {
-            updateButtonGravity(mCurrentPipBounds);
-            refocusPreviousButton();
-        }
-    }
-
-    private void setActionForButton(RemoteAction action, TvWindowMenuActionButton button,
-            Handler mainHandler) {
-        button.setVisibility(View.VISIBLE); // Ensure the button is visible.
-        if (action.getContentDescription().length() > 0) {
-            button.setTextAndDescription(action.getContentDescription());
-        } else {
-            button.setTextAndDescription(action.getTitle());
-        }
-        button.setEnabled(action.isEnabled());
-        button.setTag(action);
-        action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler);
-    }
-
-    @Nullable
-    SurfaceControl getWindowSurfaceControl() {
-        final ViewRootImpl root = getViewRootImpl();
-        if (root == null) {
-            return null;
-        }
-        final SurfaceControl out = root.getSurfaceControl();
-        if (out != null && out.isValid()) {
-            return out;
-        }
-        return null;
-    }
-
     @Override
-    public void onClick(View v) {
-        final int id = v.getId();
-        if (id == R.id.tv_pip_menu_fullscreen_button) {
-            mListener.onFullscreenButtonClick();
-        } else if (id == R.id.tv_pip_menu_move_button) {
-            mListener.onEnterMoveMode();
-        } else if (id == R.id.tv_pip_menu_close_button) {
-            mListener.onCloseButtonClick();
-        } else if (id == R.id.tv_pip_menu_expand_button) {
-            mListener.onToggleExpandedMode();
-        } else {
-            // This should be an "additional action"
-            final RemoteAction action = (RemoteAction) v.getTag();
-            if (action != null) {
-                try {
-                    action.getActionIntent().send();
-                } catch (PendingIntent.CanceledException e) {
-                    ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                            "%s: Failed to send action, %s", TAG, e);
-                }
-            } else {
-                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                        "%s: RemoteAction is null", TAG);
-            }
+    public void onActionsChanged(int added, int updated, int startIndex) {
+        mRecyclerViewAdapter.notifyItemRangeChanged(startIndex, updated);
+        if (added > 0) {
+            mRecyclerViewAdapter.notifyItemRangeInserted(startIndex + updated, added);
+        } else if (added < 0) {
+            mRecyclerViewAdapter.notifyItemRangeRemoved(startIndex + updated, -added);
         }
     }
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
         if (event.getAction() == ACTION_UP) {
-            if (!mMoveMenuIsVisible) {
-                mFocusedButton = mActionButtonsContainer.getFocusedChild();
-            }
 
             if (event.getKeyCode() == KEYCODE_BACK) {
                 mListener.onBackPress();
@@ -624,10 +402,6 @@
     public void showMovementHints(int gravity) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
-
-        if (mMoveMenuIsVisible) {
-            return;
-        }
         mMoveMenuIsVisible = true;
 
         animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
@@ -643,9 +417,12 @@
 
         animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
         if (a11yEnabled) {
+            mA11yDoneButton.setVisibility(VISIBLE);
             mA11yDoneButton.setOnClickListener(v -> {
                 mListener.onExitMoveMode();
             });
+            mA11yDoneButton.requestFocus();
+            mA11yDoneButton.requestAccessibilityFocus();
         }
     }
 
@@ -684,33 +461,67 @@
     /**
      * Show or hide the pip buttons menu.
      */
-    public void showButtonsMenu(boolean show) {
+    private void setMenuButtonsVisible(boolean visible) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: showUserActions: %b", TAG, show);
-        if (mButtonMenuIsVisible == show) {
-            return;
-        }
-        mButtonMenuIsVisible = show;
-
-        if (show) {
-            mActionButtonsContainer.setVisibility(VISIBLE);
-            refocusPreviousButton();
-        }
-        animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
-        animateAlphaTo(show ? 1 : 0, mDimLayer);
-        mEduTextDrawer.closeIfNeeded();
+                "%s: showUserActions: %b", TAG, visible);
+        mButtonMenuIsVisible = visible;
+        animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView);
     }
 
     private void setFrameHighlighted(boolean highlighted) {
         mMenuFrameView.setActivated(highlighted);
     }
 
+    private class RecyclerViewAdapter extends
+            RecyclerView.Adapter<RecyclerViewAdapter.ButtonViewHolder> {
+
+        private final List<TvPipAction> mActionList;
+
+        RecyclerViewAdapter(List<TvPipAction> actionList) {
+            this.mActionList = actionList;
+        }
+
+        @NonNull
+        @Override
+        public ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            return new ButtonViewHolder(new TvWindowMenuActionButton(mContext));
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) {
+            TvPipAction action = mActionList.get(position);
+            action.populateButton(holder.mButton, mMainHandler);
+        }
+
+        @Override
+        public int getItemCount() {
+            return mActionList.size();
+        }
+
+        private class ButtonViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
+            TvWindowMenuActionButton mButton;
+
+            ButtonViewHolder(@NonNull View itemView) {
+                super(itemView);
+                mButton = (TvWindowMenuActionButton) itemView;
+                mButton.setOnClickListener(this);
+            }
+
+            @Override
+            public void onClick(View v) {
+                TvPipAction action = mActionList.get(
+                        mActionButtonsRecyclerView.getChildLayoutPosition(v));
+                if (action != null) {
+                    action.executeAction();
+                }
+            }
+        }
+    }
+
     interface Listener extends TvPipMenuEduTextDrawer.Listener {
 
         void onBackPress();
 
-        void onEnterMoveMode();
-
         /**
          * Called when a button for exiting move mode was pressed.
          *
@@ -723,11 +534,5 @@
          * @return whether pip movement was handled.
          */
         boolean onPipMovement(int keycode);
-
-        void onCloseButtonClick();
-
-        void onFullscreenButtonClick();
-
-        void onToggleExpandedMode();
     }
-}
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
index e3308f0..f22ee59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
@@ -16,18 +16,13 @@
 
 package com.android.wm.shell.pip.tv;
 
-import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE;
-import static android.app.Notification.Action.SEMANTIC_ACTION_NONE;
-
+import android.annotation.NonNull;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.app.RemoteAction;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
@@ -35,7 +30,6 @@
 import android.graphics.drawable.Icon;
 import android.media.session.MediaSession;
 import android.os.Bundle;
-import android.os.Handler;
 import android.text.TextUtils;
 
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
@@ -47,7 +41,6 @@
 import com.android.wm.shell.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -55,39 +48,18 @@
  * <p>Once it's created, it will manage the PiP notification UI by itself except for handling
  * configuration changes and user initiated expanded PiP toggling.
  */
-public class TvPipNotificationController {
-    private static final String TAG = "TvPipNotification";
+public class TvPipNotificationController implements TvPipActionsProvider.Listener {
+    private static final String TAG = TvPipNotificationController.class.getSimpleName();
 
     // Referenced in com.android.systemui.util.NotificationChannels.
     public static final String NOTIFICATION_CHANNEL = "TVPIP";
     private static final String NOTIFICATION_TAG = "TvPip";
-    private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
-
-    private static final String ACTION_SHOW_PIP_MENU =
-            "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
-    private static final String ACTION_CLOSE_PIP =
-            "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP";
-    private static final String ACTION_MOVE_PIP =
-            "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
-    private static final String ACTION_TOGGLE_EXPANDED_PIP =
-            "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP";
-    private static final String ACTION_FULLSCREEN =
-            "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN";
 
     private final Context mContext;
     private final PackageManager mPackageManager;
     private final NotificationManager mNotificationManager;
     private final Notification.Builder mNotificationBuilder;
-    private final ActionBroadcastReceiver mActionBroadcastReceiver;
-    private final Handler mMainHandler;
-    private Delegate mDelegate;
-    private final TvPipBoundsState mTvPipBoundsState;
-
-    private String mDefaultTitle;
-
-    private final List<RemoteAction> mCustomActions = new ArrayList<>();
-    private final List<RemoteAction> mMediaActions = new ArrayList<>();
-    private RemoteAction mCustomCloseAction;
+    private TvPipActionsProvider mTvPipActionsProvider;
 
     private MediaSession.Token mMediaSessionToken;
 
@@ -95,19 +67,23 @@
     private String mPackageName;
 
     private boolean mIsNotificationShown;
+    private String mDefaultTitle;
     private String mPipTitle;
     private String mPipSubtitle;
 
+    // Saving the actions, so they don't have to be regenerated when e.g. the PiP title changes.
+    @NonNull
+    private Notification.Action[] mPipActions;
+
     private Bitmap mActivityIcon;
 
     public TvPipNotificationController(Context context, PipMediaController pipMediaController,
-            PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState,
-            Handler mainHandler) {
+            PipParamsChangedForwarder pipParamsChangedForwarder) {
         mContext = context;
         mPackageManager = context.getPackageManager();
         mNotificationManager = context.getSystemService(NotificationManager.class);
-        mMainHandler = mainHandler;
-        mTvPipBoundsState = tvPipBoundsState;
+
+        mPipActions = new Notification.Action[0];
 
         mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL)
                 .setLocalOnly(true)
@@ -117,34 +93,15 @@
                 .setOnlyAlertOnce(true)
                 .setSmallIcon(R.drawable.pip_icon)
                 .setAllowSystemGeneratedContextualActions(false)
-                .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN))
-                .setDeleteIntent(getCloseAction().actionIntent)
-                .extend(new Notification.TvExtender()
-                        .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU))
-                        .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP)));
+                .setContentIntent(
+                        createPendingIntent(context, TvPipController.ACTION_TO_FULLSCREEN));
+        // TvExtender and DeleteIntent set later since they might change.
 
-        mActionBroadcastReceiver = new ActionBroadcastReceiver();
-
-        pipMediaController.addActionListener(this::onMediaActionsChanged);
         pipMediaController.addTokenListener(this::onMediaSessionTokenChanged);
 
         pipParamsChangedForwarder.addListener(
                 new PipParamsChangedForwarder.PipParamsChangedCallback() {
                     @Override
-                    public void onExpandedAspectRatioChanged(float ratio) {
-                        updateExpansionState();
-                    }
-
-                    @Override
-                    public void onActionsChanged(List<RemoteAction> actions,
-                            RemoteAction closeAction) {
-                        mCustomActions.clear();
-                        mCustomActions.addAll(actions);
-                        mCustomCloseAction = closeAction;
-                        updateNotificationContent();
-                    }
-
-                    @Override
                     public void onTitleChanged(String title) {
                         mPipTitle = title;
                         updateNotificationContent();
@@ -157,34 +114,33 @@
                     }
                 });
 
-        onConfigurationChanged(context);
+        onConfigurationChanged();
     }
 
-    void setDelegate(Delegate delegate) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s",
-                TAG, delegate);
+    /**
+     * Call before showing any notification.
+     */
+    void setTvPipActionsProvider(@NonNull TvPipActionsProvider tvPipActionsProvider) {
+        mTvPipActionsProvider = tvPipActionsProvider;
+        mTvPipActionsProvider.addListener(this);
+    }
 
-        if (mDelegate != null) {
-            throw new IllegalStateException(
-                    "The delegate has already been set and should not change.");
-        }
-        if (delegate == null) {
-            throw new IllegalArgumentException("The delegate must not be null.");
-        }
-
-        mDelegate = delegate;
+    void onConfigurationChanged() {
+        mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title);
+        updateNotificationContent();
     }
 
     void show(String packageName) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName);
-        if (mDelegate == null) {
-            throw new IllegalStateException("Delegate is not set.");
+        if (mTvPipActionsProvider == null) {
+            ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Missing TvPipActionsProvider", TAG);
+            return;
         }
 
         mIsNotificationShown = true;
         mPackageName = packageName;
         mActivityIcon = getActivityIcon();
-        mActionBroadcastReceiver.register();
 
         updateNotificationContent();
     }
@@ -194,151 +150,42 @@
 
         mIsNotificationShown = false;
         mPackageName = null;
-        mActionBroadcastReceiver.unregister();
-
         mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
     }
 
-    private Notification.Action getToggleAction(boolean expanded) {
-        if (expanded) {
-            return createSystemAction(R.drawable.pip_ic_collapse,
-                    R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP);
-        } else {
-            return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand,
-                    ACTION_TOGGLE_EXPANDED_PIP);
-        }
-    }
-
-    private Notification.Action createSystemAction(int iconRes, int titleRes, String action) {
-        Notification.Action.Builder builder = new Notification.Action.Builder(
-                Icon.createWithResource(mContext, iconRes),
-                mContext.getString(titleRes),
-                createPendingIntent(mContext, action));
-        builder.setContextual(true);
-        return builder.build();
-    }
-
-    private void onMediaActionsChanged(List<RemoteAction> actions) {
-        mMediaActions.clear();
-        mMediaActions.addAll(actions);
-        if (mCustomActions.isEmpty()) {
-            updateNotificationContent();
-        }
-    }
-
     private void onMediaSessionTokenChanged(MediaSession.Token token) {
         mMediaSessionToken = token;
         updateNotificationContent();
     }
 
-    private Notification.Action remoteToNotificationAction(RemoteAction action) {
-        return remoteToNotificationAction(action, SEMANTIC_ACTION_NONE);
-    }
-
-    private Notification.Action remoteToNotificationAction(RemoteAction action,
-            int semanticAction) {
-        Notification.Action.Builder builder = new Notification.Action.Builder(action.getIcon(),
-                action.getTitle(),
-                action.getActionIntent());
-        if (action.getContentDescription() != null) {
-            Bundle extras = new Bundle();
-            extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION,
-                    action.getContentDescription());
-            builder.addExtras(extras);
-        }
-        builder.setSemanticAction(semanticAction);
-        builder.setContextual(true);
-        return builder.build();
-    }
-
-    private Notification.Action[] getNotificationActions() {
-        final List<Notification.Action> actions = new ArrayList<>();
-
-        // 1. Fullscreen
-        actions.add(getFullscreenAction());
-        // 2. Close
-        actions.add(getCloseAction());
-        // 3. App actions
-        final List<RemoteAction> appActions =
-                mCustomActions.isEmpty() ? mMediaActions : mCustomActions;
-        for (RemoteAction appAction : appActions) {
-            if (PipUtils.remoteActionsMatch(mCustomCloseAction, appAction)
-                    || !appAction.isEnabled()) {
-                continue;
-            }
-            actions.add(remoteToNotificationAction(appAction));
-        }
-        // 4. Move
-        actions.add(getMoveAction());
-        // 5. Toggle expansion (if expanded PiP enabled)
-        if (mTvPipBoundsState.getDesiredTvExpandedAspectRatio() > 0
-                && mTvPipBoundsState.isTvExpandedPipSupported()) {
-            actions.add(getToggleAction(mTvPipBoundsState.isTvPipExpanded()));
-        }
-        return actions.toArray(new Notification.Action[0]);
-    }
-
-    private Notification.Action getCloseAction() {
-        if (mCustomCloseAction == null) {
-            return createSystemAction(R.drawable.pip_ic_close_white, R.string.pip_close,
-                    ACTION_CLOSE_PIP);
-        } else {
-            return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE);
-        }
-    }
-
-    private Notification.Action getFullscreenAction() {
-        return createSystemAction(R.drawable.pip_ic_fullscreen_white,
-                R.string.pip_fullscreen, ACTION_FULLSCREEN);
-    }
-
-    private Notification.Action getMoveAction() {
-        return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move,
-                ACTION_MOVE_PIP);
-    }
-
-    /**
-     * Called by {@link TvPipController} when the configuration is changed.
-     */
-    void onConfigurationChanged(Context context) {
-        mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title);
-        updateNotificationContent();
-    }
-
-    void updateExpansionState() {
-        updateNotificationContent();
-    }
-
     private void updateNotificationContent() {
         if (mPackageManager == null || !mIsNotificationShown) {
             return;
         }
 
-        Notification.Action[] actions = getNotificationActions();
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: update(), title: %s, subtitle: %s, mediaSessionToken: %s, #actions: %s", TAG,
-                getNotificationTitle(), mPipSubtitle, mMediaSessionToken, actions.length);
-        for (Notification.Action action : actions) {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: action: %s", TAG,
-                    action.toString());
-        }
-
+                getNotificationTitle(), mPipSubtitle, mMediaSessionToken, mPipActions.length);
         mNotificationBuilder
                 .setWhen(System.currentTimeMillis())
                 .setContentTitle(getNotificationTitle())
                 .setContentText(mPipSubtitle)
                 .setSubText(getApplicationLabel(mPackageName))
-                .setActions(actions);
+                .setActions(mPipActions);
         setPipIcon();
 
         Bundle extras = new Bundle();
         extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken);
         mNotificationBuilder.setExtras(extras);
 
+        PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent();
+        mNotificationBuilder.setDeleteIntent(closeIntent);
         // TvExtender not recognized if not set last.
         mNotificationBuilder.extend(new Notification.TvExtender()
-                .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU))
-                .setDeleteIntent(createPendingIntent(mContext, ACTION_CLOSE_PIP)));
+                .setContentIntent(
+                        createPendingIntent(mContext, TvPipController.ACTION_SHOW_PIP_MENU))
+                .setDeleteIntent(closeIntent));
+
         mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP,
                 mNotificationBuilder.build());
     }
@@ -390,68 +237,20 @@
         return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true);
     }
 
-    private static PendingIntent createPendingIntent(Context context, String action) {
+    static PendingIntent createPendingIntent(Context context, String action) {
         return PendingIntent.getBroadcast(context, 0,
                 new Intent(action).setPackage(context.getPackageName()),
                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
     }
 
-    private class ActionBroadcastReceiver extends BroadcastReceiver {
-        final IntentFilter mIntentFilter;
-        {
-            mIntentFilter = new IntentFilter();
-            mIntentFilter.addAction(ACTION_CLOSE_PIP);
-            mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
-            mIntentFilter.addAction(ACTION_MOVE_PIP);
-            mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP);
-            mIntentFilter.addAction(ACTION_FULLSCREEN);
+    @Override
+    public void onActionsChanged(int added, int updated, int startIndex) {
+        List<TvPipAction> actions = mTvPipActionsProvider.getActionsList();
+        mPipActions = new Notification.Action[actions.size()];
+        for (int i = 0; i < mPipActions.length; i++) {
+            mPipActions[i] = actions.get(i).toNotificationAction(mContext);
         }
-        boolean mRegistered = false;
-
-        void register() {
-            if (mRegistered) return;
-
-            mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION,
-                    mMainHandler);
-            mRegistered = true;
-        }
-
-        void unregister() {
-            if (!mRegistered) return;
-
-            mContext.unregisterReceiver(this);
-            mRegistered = false;
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: on(Broadcast)Receive(), action=%s", TAG, action);
-
-            if (ACTION_SHOW_PIP_MENU.equals(action)) {
-                mDelegate.showPictureInPictureMenu();
-            } else if (ACTION_CLOSE_PIP.equals(action)) {
-                mDelegate.closePip();
-            } else if (ACTION_MOVE_PIP.equals(action)) {
-                mDelegate.enterPipMovementMenu();
-            } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) {
-                mDelegate.togglePipExpansion();
-            } else if (ACTION_FULLSCREEN.equals(action)) {
-                mDelegate.movePipToFullscreen();
-            }
-        }
+        updateNotificationContent();
     }
 
-    interface Delegate {
-        void showPictureInPictureMenu();
-
-        void closePip();
-
-        void enterPipMovementMenu();
-
-        void togglePipExpansion();
-
-        void movePipToFullscreen();
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java
new file mode 100644
index 0000000..93b6a90
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE;
+import static android.app.Notification.Action.SEMANTIC_ACTION_NONE;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.StringRes;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+
+import com.android.wm.shell.common.TvWindowMenuActionButton;
+
+/**
+ * A TvPipAction for actions that the system provides, i.e. fullscreen, default close, move,
+ * expand/collapse.
+ */
+public class TvPipSystemAction extends TvPipAction {
+
+    @StringRes
+    private int mTitleResource;
+    @DrawableRes
+    private int mIconResource;
+
+    private final PendingIntent mBroadcastIntent;
+
+    TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon,
+            String broadcastAction, @NonNull Context context,
+            SystemActionsHandler systemActionsHandler) {
+        super(actionType, systemActionsHandler);
+        update(title, icon);
+        mBroadcastIntent = TvPipNotificationController.createPendingIntent(context,
+                broadcastAction);
+    }
+
+    void update(@StringRes int title, @DrawableRes int icon) {
+        mTitleResource = title;
+        mIconResource = icon;
+    }
+
+    void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) {
+        button.setTextAndDescription(mTitleResource);
+        button.setImageResource(mIconResource);
+        button.setEnabled(true);
+    }
+
+    PendingIntent getPendingIntent() {
+        return mBroadcastIntent;
+    }
+
+    @Override
+    Notification.Action toNotificationAction(Context context) {
+        Notification.Action.Builder builder = new Notification.Action.Builder(
+                Icon.createWithResource(context, mIconResource),
+                context.getString(mTitleResource),
+                mBroadcastIntent);
+
+        builder.setSemanticAction(isCloseAction()
+                ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE);
+        builder.setContextual(true);
+        return builder.build();
+    }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index e7ec15e..89538cb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -16,9 +16,6 @@
 
 package com.android.wm.shell.splitscreen;
 
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
-
 import android.content.Context;
 import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
@@ -34,8 +31,6 @@
  * @see StageCoordinator
  */
 class MainStage extends StageTaskListener {
-    private static final String TAG = MainStage.class.getSimpleName();
-
     private boolean mIsActive = false;
 
     MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
@@ -52,15 +47,8 @@
     void activate(WindowContainerTransaction wct, boolean includingTopTask) {
         if (mIsActive) return;
 
-        final WindowContainerToken rootToken = mRootTaskInfo.token;
         if (includingTopTask) {
-            wct.reparentTasks(
-                    null /* currentParent */,
-                    rootToken,
-                    CONTROLLED_WINDOWING_MODES,
-                    CONTROLLED_ACTIVITY_TYPES,
-                    true /* onTop */,
-                    true /* reparentTopOnly */);
+            reparentTopTask(wct);
         }
 
         mIsActive = true;
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 b26bc9c..ef70d9b 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
@@ -121,7 +121,8 @@
     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;
+    public static final int EXIT_REASON_RECREATE_SPLIT = 10;
+    public static final int EXIT_REASON_FULLSCREEN_SHORTCUT = 11;
     @IntDef(value = {
             EXIT_REASON_UNKNOWN,
             EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW,
@@ -133,6 +134,7 @@
             EXIT_REASON_SCREEN_LOCKED,
             EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP,
             EXIT_REASON_CHILD_TASK_ENTER_PIP,
+            EXIT_REASON_RECREATE_SPLIT,
             EXIT_REASON_FULLSCREEN_SHORTCUT,
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -470,7 +472,7 @@
      */
     public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
             @Nullable Bundle options, UserHandle user, @NonNull InstanceId instanceId) {
-        mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER);
+        mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER);
         startShortcut(packageName, shortcutId, position, options, user);
     }
 
@@ -518,7 +520,7 @@
      */
     public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
             @SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) {
-        mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER);
+        mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER);
         startIntent(intent, fillInIntent, position, options);
     }
 
@@ -784,10 +786,10 @@
         return splitTasksLayer;
     }
     /**
-     * Sets drag info to be logged when splitscreen is entered.
+     * Drop callback when splitscreen is entered.
      */
-    public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
-        mStageCoordinator.logOnDroppedToSplit(position, dragSessionId);
+    public void onDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+        mStageCoordinator.onDroppedToSplit(position, dragSessionId);
     }
 
     /**
@@ -815,6 +817,8 @@
                 return "APP_DOES_NOT_SUPPORT_MULTIWINDOW";
             case EXIT_REASON_CHILD_TASK_ENTER_PIP:
                 return "CHILD_TASK_ENTER_PIP";
+            case EXIT_REASON_RECREATE_SPLIT:
+                return "RECREATE_SPLIT";
             default:
                 return "unknown reason, reason int = " + exitReason;
         }
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 1016e1b..5483fa5 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
@@ -21,9 +21,11 @@
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP;
 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__RECREATE_SPLIT;
 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;
@@ -37,9 +39,11 @@
 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_UNKNOWN;
 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_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;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT;
 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;
@@ -182,6 +186,10 @@
                 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_CHILD_TASK_ENTER_PIP:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP;
+            case EXIT_REASON_RECREATE_SPLIT:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT;
             case EXIT_REASON_FULLSCREEN_SHORTCUT:
                 return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
             case EXIT_REASON_UNKNOWN:
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 da8dc87..5be29db 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,10 +49,11 @@
 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;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT;
 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_SHOW_ON_TOP;
@@ -199,7 +200,8 @@
     // and exit, since exit itself can trigger a number of changes that update the stages.
     private boolean mShouldUpdateRecents;
     private boolean mExitSplitScreenOnHide;
-    private boolean mIsDividerRemoteAnimating;
+    private boolean mIsSplitEntering;
+    private boolean mIsDropEntering;
     private boolean mIsExiting;
 
     /** The target stage to dismiss to when unlock after folded. */
@@ -347,10 +349,14 @@
         return mSplitTransitions;
     }
 
-    boolean isSplitScreenVisible() {
+    public boolean isSplitScreenVisible() {
         return mSideStageListener.mVisible && mMainStageListener.mVisible;
     }
 
+    public boolean isSplitActive() {
+        return mMainStage.isActive();
+    }
+
     @StageType
     int getStageOfTask(int taskId) {
         if (mMainStage.containsTask(taskId)) {
@@ -373,11 +379,14 @@
             targetStage = mSideStage;
             sideStagePosition = stagePosition;
         } else {
-            if (mMainStage.isActive()) {
+            if (isSplitScreenVisible()) {
                 // If the split screen is activated, retrieves target stage based on position.
                 targetStage = stagePosition == mSideStagePosition ? mSideStage : mMainStage;
                 sideStagePosition = mSideStagePosition;
             } else {
+                // Exit split if it running background.
+                exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
+
                 targetStage = mSideStage;
                 sideStagePosition = stagePosition;
             }
@@ -673,6 +682,10 @@
             @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent,
             @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
             RemoteAnimationAdapter adapter, InstanceId instanceId) {
+        if (!isSplitScreenVisible()) {
+            exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
+        }
+
         // Init divider first to make divider leash for remote animation target.
         mSplitLayout.init();
         mSplitLayout.setDivideRatio(splitRatio);
@@ -685,11 +698,13 @@
 
         // Set false to avoid record new bounds with old task still on top;
         mShouldUpdateRecents = false;
-        mIsDividerRemoteAnimating = true;
+        mIsSplitEntering = true;
 
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
-        prepareEvictChildTasks(SPLIT_POSITION_TOP_OR_LEFT, evictWct);
-        prepareEvictChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, evictWct);
+        if (isSplitScreenVisible()) {
+            mMainStage.evictAllChildren(evictWct);
+            mSideStage.evictAllChildren(evictWct);
+        }
 
         IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
             @Override
@@ -769,7 +784,7 @@
 
     private void onRemoteAnimationFinishedOrCancelled(boolean cancel,
             WindowContainerTransaction evictWct) {
-        mIsDividerRemoteAnimating = false;
+        mIsSplitEntering = false;
         mShouldUpdateRecents = true;
         // If any stage has no child after animation finished, it means that split will display
         // nothing, such status will happen if task and intent is same app but not support
@@ -781,6 +796,9 @@
             mSplitUnsupportedToast.show();
         } else {
             mSyncQueue.queue(evictWct);
+            mSyncQueue.runInSync(t -> {
+                updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+            });
         }
     }
 
@@ -815,7 +833,7 @@
         switch (stage) {
             case STAGE_TYPE_UNDEFINED: {
                 if (position != SPLIT_POSITION_UNDEFINED) {
-                    if (mMainStage.isActive()) {
+                    if (isSplitScreenVisible()) {
                         // Use the stage of the specified position
                         options = resolveStartStage(
                                 position == mSideStagePosition ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN,
@@ -1045,14 +1063,13 @@
             }
         });
         mShouldUpdateRecents = false;
-        mIsDividerRemoteAnimating = false;
+        mIsSplitEntering = false;
 
         mSplitLayout.getInvisibleBounds(mTempRect1);
         if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) {
             mSideStage.removeAllTasks(wct, false /* toTop */);
             mMainStage.deactivate(wct, false /* toTop */);
             wct.reorder(mRootTaskInfo.token, false /* onTop */);
-            wct.setForceTranslucent(mRootTaskInfo.token, true);
             wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
             onTransitionAnimationComplete();
         } else {
@@ -1064,6 +1081,8 @@
             wct.setSmallestScreenWidthDp(childrenToTop.mRootTaskInfo.token,
                     SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
         }
+        wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+                false /* reparentLeafTaskIfRelaunch */);
         mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t -> {
             t.setWindowCrop(mMainStage.mRootLeash, null)
@@ -1082,7 +1101,6 @@
                     mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */);
                     mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */);
                     finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
-                    finishedWCT.setForceTranslucent(mRootTaskInfo.token, true);
                     finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
                     mSyncQueue.queue(finishedWCT);
                     mSyncQueue.runInSync(at -> {
@@ -1374,7 +1392,7 @@
                 && !ENABLE_SHELL_TRANSITIONS) {
             // Clear the divider remote animating flag as the divider will be re-rendered to apply
             // the new rotation config.
-            mIsDividerRemoteAnimating = false;
+            mIsSplitEntering = false;
             mSplitLayout.update(null /* t */);
             onLayoutSizeChanged(mSplitLayout);
         }
@@ -1423,6 +1441,36 @@
         });
     }
 
+    void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) {
+        if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive()
+                && !mIsSplitEntering) {
+            // Handle entring split case here if split already running background.
+            if (mIsDropEntering) {
+                mSplitLayout.resetDividerPosition();
+            } else {
+                mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+            }
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            mMainStage.reparentTopTask(wct);
+            mMainStage.evictAllChildren(wct);
+            mSideStage.evictOtherChildren(wct, taskId);
+            updateWindowBounds(mSplitLayout, wct);
+            wct.reorder(mRootTaskInfo.token, true);
+            wct.setForceTranslucent(mRootTaskInfo.token, false);
+
+            mSyncQueue.queue(wct);
+            mSyncQueue.runInSync(t -> {
+                if (mIsDropEntering) {
+                    updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+                    mIsDropEntering = false;
+                } else {
+                    mShowDecorImmediately = true;
+                    mSplitLayout.flingDividerToCenter();
+                }
+            });
+        }
+    }
+
     private void onRootTaskVanished() {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (mRootTaskInfo != null) {
@@ -1441,20 +1489,22 @@
             return;
         }
 
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (!mainStageVisible) {
+            wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+                    true /* setReparentLeafTaskIfRelaunch */);
+            wct.setForceTranslucent(mRootTaskInfo.token, true);
             // Both stages are not visible, check if it needs to dismiss split screen.
-            if (mExitSplitScreenOnHide
-                    // Don't dismiss split screen when both stages are not visible due to sleeping
-                    // display.
-                    || (!mMainStage.mRootTaskInfo.isSleeping
-                    && !mSideStage.mRootTaskInfo.isSleeping)) {
+            if (mExitSplitScreenOnHide) {
                 exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME);
             }
+        } else {
+            wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+                    false /* setReparentLeafTaskIfRelaunch */);
+            wct.setForceTranslucent(mRootTaskInfo.token, false);
         }
-
+        mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t -> {
-            t.setVisibility(mSideStage.mRootLeash, sideStageVisible)
-                    .setVisibility(mMainStage.mRootLeash, mainStageVisible);
             setDividerVisibility(mainStageVisible, t);
         });
     }
@@ -1479,7 +1529,7 @@
         mDividerVisible = visible;
         sendSplitVisibilityChanged();
 
-        if (mIsDividerRemoteAnimating) {
+        if (mIsSplitEntering) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                     "   Skip animating divider bar due to it's remote animating.");
             return;
@@ -1499,7 +1549,7 @@
                     "   Skip animating divider bar due to divider leash not ready.");
             return;
         }
-        if (mIsDividerRemoteAnimating) {
+        if (mIsSplitEntering) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                     "   Skip animating divider bar due to it's remote animating.");
             return;
@@ -1555,26 +1605,21 @@
         if (!hasChildren && !mIsExiting && mMainStage.isActive()) {
             if (isSideStage && mMainStageListener.mVisible) {
                 // Exit to main stage if side stage no longer has children.
-                if (ENABLE_SHELL_TRANSITIONS) {
-                    exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED);
-                } else {
-                    mSplitLayout.flingDividerToDismiss(
-                            mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT,
-                            EXIT_REASON_APP_FINISHED);
-                }
+                mSplitLayout.flingDividerToDismiss(
+                        mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                        EXIT_REASON_APP_FINISHED);
             } else if (!isSideStage && mSideStageListener.mVisible) {
                 // Exit to side stage if main stage no longer has children.
-                if (ENABLE_SHELL_TRANSITIONS) {
-                    exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED);
-                } else {
-                    mSplitLayout.flingDividerToDismiss(
-                            mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT,
-                            EXIT_REASON_APP_FINISHED);
-                }
+                mSplitLayout.flingDividerToDismiss(
+                        mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                        EXIT_REASON_APP_FINISHED);
+            } else if (!isSplitScreenVisible()) {
+                exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED);
             }
         } else if (isSideStage && hasChildren && !mMainStage.isActive()) {
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             mSplitLayout.init();
+
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
             if (mLogger.isEnterRequestedByDrag()) {
                 prepareEnterSplitScreen(wct);
             } else {
@@ -1589,8 +1634,9 @@
 
             mSyncQueue.queue(wct);
             mSyncQueue.runInSync(t -> {
-                if (mLogger.isEnterRequestedByDrag()) {
+                if (mIsDropEntering) {
                     updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+                    mIsDropEntering = false;
                 } else {
                     mShowDecorImmediately = true;
                     mSplitLayout.flingDividerToCenter();
@@ -1945,10 +1991,6 @@
         }
     }
 
-    public boolean isSplitActive() {
-        return mMainStage.isActive();
-    }
-
     @Override
     public void mergeAnimation(IBinder transition, TransitionInfo info,
             SurfaceControl.Transaction t, IBinder mergeTarget,
@@ -2304,11 +2346,29 @@
     /**
      * Sets drag info to be logged when splitscreen is next entered.
      */
-    public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+    public void onDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+        if (!isSplitScreenVisible()) {
+            mIsDropEntering = true;
+        }
+        if (!isSplitScreenVisible()) {
+            // If split running background, exit split first.
+            exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
+        }
         mLogger.enterRequestedByDrag(position, dragSessionId);
     }
 
     /**
+     * Sets info to be logged when splitscreen is next entered.
+     */
+    public void onRequestToSplit(InstanceId sessionId, int enterReason) {
+        if (!isSplitScreenVisible()) {
+            // If split running background, exit split first.
+            exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
+        }
+        mLogger.enterRequested(sessionId, enterReason);
+    }
+
+    /**
      * Logs the exit of splitscreen.
      */
     private void logExit(@ExitReason int exitReason) {
@@ -2343,6 +2403,11 @@
         }
 
         @Override
+        public void onChildTaskAppeared(int taskId) {
+            StageCoordinator.this.onChildTaskAppeared(this, taskId);
+        }
+
+        @Override
         public void onStatusChanged(boolean visible, boolean hasChildren) {
             if (!mHasRootTask) return;
 
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 8a52c87..a841b7f 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
@@ -22,6 +22,7 @@
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
@@ -69,6 +70,8 @@
     public interface StageListenerCallbacks {
         void onRootTaskAppeared();
 
+        void onChildTaskAppeared(int taskId);
+
         void onStatusChanged(boolean visible, boolean hasChildren);
 
         void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
@@ -185,6 +188,7 @@
                 // Status is managed/synchronized by the transition lifecycle.
                 return;
             }
+            mCallbacks.onChildTaskAppeared(taskId);
             sendStatusChanged();
         } else {
             throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
@@ -338,6 +342,14 @@
         }
     }
 
+    void evictOtherChildren(WindowContainerTransaction wct, int taskId) {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) {
+            final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
+            if (taskId == taskInfo.taskId) continue;
+            wct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
+        }
+    }
+
     void evictNonOpeningChildren(RemoteAnimationTarget[] apps, WindowContainerTransaction wct) {
         final SparseArray<ActivityManager.RunningTaskInfo> toBeEvict = mChildrenTaskInfo.clone();
         for (int i = 0; i < apps.length; i++) {
@@ -360,6 +372,12 @@
         }
     }
 
+    void reparentTopTask(WindowContainerTransaction wct) {
+        wct.reparentTasks(null /* currentParent */, mRootTaskInfo.token,
+                CONTROLLED_WINDOWING_MODES, CONTROLLED_ACTIVITY_TYPES,
+                true /* onTop */, true /* reparentTopOnly */);
+    }
+
     void resetBounds(WindowContainerTransaction wct) {
         wct.setBounds(mRootTaskInfo.token, null);
         wct.setAppBounds(mRootTaskInfo.token, null);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 3cba929..a2d7bc4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -111,7 +111,7 @@
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
             @NonNull TransitionRequestInfo request) {
-        if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitActive()) {
+        if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitScreenVisible()) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request while "
                     + "Split-Screen is active, so treat it as Mixed.");
             if (request.getRemoteTransition() != null) {
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 afefd5d..42e2b3f 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
@@ -46,6 +46,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
@@ -56,7 +57,6 @@
 import com.android.wm.shell.transition.Transitions;
 
 import java.util.Optional;
-import java.util.function.Supplier;
 
 /**
  * View model for the window decoration with a caption and shadows. Works with
@@ -66,7 +66,6 @@
 public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
     private static final String TAG = "CaptionViewModel";
     private final CaptionWindowDecoration.Factory mCaptionWindowDecorFactory;
-    private final Supplier<InputManager> mInputManagerSupplier;
     private final ActivityTaskManager mActivityTaskManager;
     private final ShellTaskOrganizer mTaskOrganizer;
     private final Context mContext;
@@ -82,7 +81,7 @@
 
     private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
     private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
-    private EventReceiverFactory mEventReceiverFactory = new EventReceiverFactory();
+    private InputMonitorFactory mInputMonitorFactory;
 
     public CaptionWindowDecorViewModel(
             Context context,
@@ -101,10 +100,11 @@
                 syncQueue,
                 desktopModeController,
                 new CaptionWindowDecoration.Factory(),
-                InputManager::getInstance);
+                new InputMonitorFactory());
     }
 
-    public CaptionWindowDecorViewModel(
+    @VisibleForTesting
+    CaptionWindowDecorViewModel(
             Context context,
             Handler mainHandler,
             Choreographer mainChoreographer,
@@ -113,8 +113,7 @@
             SyncTransactionQueue syncQueue,
             Optional<DesktopModeController> desktopModeController,
             CaptionWindowDecoration.Factory captionWindowDecorFactory,
-            Supplier<InputManager> inputManagerSupplier) {
-
+            InputMonitorFactory inputMonitorFactory) {
         mContext = context;
         mMainHandler = mainHandler;
         mMainChoreographer = mainChoreographer;
@@ -125,11 +124,7 @@
         mDesktopModeController = desktopModeController;
 
         mCaptionWindowDecorFactory = captionWindowDecorFactory;
-        mInputManagerSupplier = inputManagerSupplier;
-    }
-
-    void setEventReceiverFactory(EventReceiverFactory eventReceiverFactory) {
-        mEventReceiverFactory = eventReceiverFactory;
+        mInputMonitorFactory = inputMonitorFactory;
     }
 
     @Override
@@ -205,7 +200,6 @@
         decoration.close();
         int displayId = taskInfo.displayId;
         if (mEventReceiversByDisplay.contains(displayId)) {
-            EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
             removeTaskFromEventReceiver(displayId);
         }
     }
@@ -408,12 +402,6 @@
         }
     }
 
-    class EventReceiverFactory {
-        EventReceiver create(InputMonitor inputMonitor, InputChannel channel, Looper looper) {
-            return new EventReceiver(inputMonitor, channel, looper);
-        }
-    }
-
     /**
      * Handle MotionEvents relevant to focused task's caption that don't directly touch it
      *
@@ -500,11 +488,11 @@
     }
 
     private void createInputChannel(int displayId) {
-        InputManager inputManager = mInputManagerSupplier.get();
+        InputManager inputManager = InputManager.getInstance();
         InputMonitor inputMonitor =
-                inputManager.monitorGestureInput("caption-touch", mContext.getDisplayId());
-        EventReceiver eventReceiver = mEventReceiverFactory.create(
-                inputMonitor, inputMonitor.getInputChannel(), Looper.myLooper());
+                mInputMonitorFactory.create(inputManager, mContext);
+        EventReceiver eventReceiver = new EventReceiver(inputMonitor,
+                inputMonitor.getInputChannel(), Looper.myLooper());
         mEventReceiversByDisplay.put(displayId, eventReceiver);
     }
 
@@ -562,4 +550,12 @@
             mWindowDecorByTaskId.get(taskId).closeHandleMenu();
         }
     }
+
+    static class InputMonitorFactory {
+        InputMonitor create(InputManager inputManager, Context context) {
+            return inputManager.monitorGestureInput("caption-touch", context.getDisplayId());
+        }
+    }
 }
+
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 9215496..7f85988 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -33,6 +33,7 @@
 import android.view.ViewRootImpl;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
+import android.window.TaskConstants;
 import android.window.WindowContainerTransaction;
 
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -195,7 +196,9 @@
                     .setParent(mTaskSurface)
                     .build();
 
-            startT.setTrustedOverlay(mDecorationContainerSurface, true);
+            startT.setTrustedOverlay(mDecorationContainerSurface, true)
+                    .setLayer(mDecorationContainerSurface,
+                            TaskConstants.TASK_CHILD_LAYER_WINDOW_DECORATIONS);
         }
 
         final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
@@ -213,8 +216,6 @@
                         outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY)
                 .setWindowCrop(mDecorationContainerSurface,
                         outResult.mWidth, outResult.mHeight)
-                // TODO(b/244455401): Change the z-order when it's better organized
-                .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1)
                 .show(mDecorationContainerSurface);
 
         // TaskBackgroundSurface
@@ -225,6 +226,8 @@
                     .setEffectLayer()
                     .setParent(mTaskSurface)
                     .build();
+
+            startT.setLayer(mTaskBackgroundSurface, TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND);
         }
 
         float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
@@ -236,8 +239,6 @@
                         taskBounds.height())
                 .setShadowRadius(mTaskBackgroundSurface, shadowRadius)
                 .setColor(mTaskBackgroundSurface, mTmpColor)
-                // TODO(b/244455401): Change the z-order when it's better organized
-                .setLayer(mTaskBackgroundSurface, -1)
                 .show(mTaskBackgroundSurface);
 
         // CaptionContainerSurface, CaptionWindowManager
diff --git a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
index 59d9104..fac0461 100644
--- a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
@@ -19,6 +19,8 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.wm.shell.tests">
 
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+
     <application android:debuggable="true" android:largeHeap="true">
         <uses-library android:name="android.test.mock" />
         <uses-library android:name="android.test.runner" />
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java
new file mode 100644
index 0000000..91040e9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN;
+import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.graphics.drawable.Icon;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Log;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.pip.PipMediaController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link TvPipActionsProvider}
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class TvPipActionProviderTest extends ShellTestCase {
+    private static final String TAG = TvPipActionProviderTest.class.getSimpleName();
+    private TvPipActionsProvider mActionsProvider;
+
+    @Mock
+    private PipMediaController mMockPipMediaController;
+    @Mock
+    private TvPipActionsProvider.Listener mMockListener;
+    @Mock
+    private TvPipAction.SystemActionsHandler mMockSystemActionsHandler;
+    @Mock
+    private Icon mMockIcon;
+    @Mock
+    private PendingIntent mMockPendingIntent;
+
+    private RemoteAction createRemoteAction(int identifier) {
+        return new RemoteAction(mMockIcon, "" + identifier, "" + identifier, mMockPendingIntent);
+    }
+
+    private List<RemoteAction> createRemoteActions(int numberOfActions) {
+        List<RemoteAction> actions = new ArrayList<>();
+        for (int i = 0; i < numberOfActions; i++) {
+            actions.add(createRemoteAction(i));
+        }
+        return actions;
+    }
+
+    private boolean checkActionsMatch(List<TvPipAction> actions, int[] actionTypes) {
+        for (int i = 0; i < actions.size(); i++) {
+            int type = actions.get(i).getActionType();
+            if (type != actionTypes[i]) {
+                Log.e(TAG, "Action at index " + i + ": found " + type
+                        + ", expected " + actionTypes[i]);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mActionsProvider = new TvPipActionsProvider(mContext, mMockPipMediaController,
+                mMockSystemActionsHandler);
+    }
+
+    @Test
+    public void defaultSystemActions_regularPip() {
+        mActionsProvider.updateExpansionEnabled(false);
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE}));
+    }
+
+    @Test
+    public void defaultSystemActions_expandedPip() {
+        mActionsProvider.updateExpansionEnabled(true);
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE}));
+    }
+
+    @Test
+    public void expandedPip_enableExpansion_enable() {
+        // PiP has expanded PiP disabled.
+        mActionsProvider.updateExpansionEnabled(false);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.updateExpansionEnabled(true);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE}));
+        verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 0, /* index= */ 3);
+    }
+
+    @Test
+    public void expandedPip_enableExpansion_disable() {
+        mActionsProvider.updateExpansionEnabled(true);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.updateExpansionEnabled(false);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE}));
+        verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 0, /* index= */ 3);
+    }
+
+    @Test
+    public void expandedPip_enableExpansion_AlreadyEnabled() {
+        mActionsProvider.updateExpansionEnabled(true);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.updateExpansionEnabled(true);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE}));
+    }
+
+    @Test
+    public void expandedPip_toggleExpansion() {
+        // PiP has expanded PiP enabled, but is in a collapsed state
+        mActionsProvider.updateExpansionEnabled(true);
+        mActionsProvider.onPipExpansionToggled(/* expanded= */ false);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.onPipExpansionToggled(/* expanded= */ true);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE}));
+        verify(mMockListener).onActionsChanged(0, 1, 3);
+    }
+
+    @Test
+    public void customActions_added() {
+        mActionsProvider.updateExpansionEnabled(false);
+        mActionsProvider.addListener(mMockListener);
+
+        mActionsProvider.setAppActions(createRemoteActions(2), null);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+                        ACTION_MOVE}));
+        verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2);
+    }
+
+    @Test
+    public void customActions_replacedMore() {
+        mActionsProvider.updateExpansionEnabled(false);
+        mActionsProvider.setAppActions(createRemoteActions(2), null);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.setAppActions(createRemoteActions(3), null);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+                        ACTION_CUSTOM, ACTION_MOVE}));
+        verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 2, /* index= */ 2);
+    }
+
+    @Test
+    public void customActions_replacedLess() {
+        mActionsProvider.updateExpansionEnabled(false);
+        mActionsProvider.setAppActions(createRemoteActions(2), null);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.setAppActions(createRemoteActions(0), null);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE}));
+        verify(mMockListener).onActionsChanged(/* added= */ -2, /* updated= */ 0, /* index= */ 2);
+    }
+
+    @Test
+    public void customCloseAdded() {
+        mActionsProvider.updateExpansionEnabled(false);
+
+        List<RemoteAction> customActions = new ArrayList<>();
+        mActionsProvider.setAppActions(customActions, null);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.setAppActions(customActions, createRemoteAction(0));
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_MOVE}));
+        verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1);
+    }
+
+    @Test
+    public void customClose_matchesOtherCustomAction() {
+        mActionsProvider.updateExpansionEnabled(false);
+
+        List<RemoteAction> customActions = createRemoteActions(2);
+        RemoteAction customClose = createRemoteAction(/* id= */ 10);
+        customActions.add(customClose);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.setAppActions(customActions, customClose);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+                        ACTION_MOVE}));
+        verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1);
+        verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2);
+    }
+
+    @Test
+    public void mediaActions_added_whileCustomActionsExist() {
+        mActionsProvider.updateExpansionEnabled(false);
+        mActionsProvider.setAppActions(createRemoteActions(2), null);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.onMediaActionsChanged(createRemoteActions(3));
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+                        ACTION_MOVE}));
+        verify(mMockListener, times(0)).onActionsChanged(anyInt(), anyInt(), anyInt());
+    }
+
+    @Test
+    public void customActions_removed_whileMediaActionsExist() {
+        mActionsProvider.updateExpansionEnabled(false);
+        mActionsProvider.onMediaActionsChanged(createRemoteActions(2));
+        mActionsProvider.setAppActions(createRemoteActions(3), null);
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.setAppActions(createRemoteActions(0), null);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+                        ACTION_MOVE}));
+        verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 2, /* index= */ 2);
+    }
+
+    @Test
+    public void customCloseOnly_mediaActionsShowing() {
+        mActionsProvider.updateExpansionEnabled(false);
+        mActionsProvider.onMediaActionsChanged(createRemoteActions(2));
+
+        mActionsProvider.addListener(mMockListener);
+        mActionsProvider.setAppActions(createRemoteActions(0), createRemoteAction(5));
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+                        ACTION_MOVE}));
+        verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1);
+    }
+
+    @Test
+    public void customActions_showDisabledActions() {
+        mActionsProvider.updateExpansionEnabled(false);
+
+        List<RemoteAction> customActions = createRemoteActions(2);
+        customActions.get(0).setEnabled(false);
+        mActionsProvider.setAppActions(customActions, null);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM,
+                        ACTION_MOVE}));
+    }
+
+    @Test
+    public void mediaActions_hideDisabledActions() {
+        mActionsProvider.updateExpansionEnabled(false);
+
+        List<RemoteAction> customActions = createRemoteActions(2);
+        customActions.get(0).setEnabled(false);
+        mActionsProvider.onMediaActionsChanged(customActions);
+
+        assertTrue(checkActionsMatch(mActionsProvider.getActionsList(),
+                new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_MOVE}));
+    }
+
+}
+
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 ad6fced..0dbf30d 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
@@ -21,14 +21,15 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
 import android.hardware.input.InputManager;
 import android.os.Handler;
 import android.os.Looper;
@@ -37,9 +38,9 @@
 import android.view.InputChannel;
 import android.view.InputMonitor;
 import android.view.SurfaceControl;
+import android.view.SurfaceView;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.rule.GrantPermissionRule;
 
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
@@ -55,37 +56,28 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
-import java.util.function.Supplier;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /** Tests of {@link CaptionWindowDecorViewModel} */
 @SmallTest
 public class CaptionWindowDecorViewModelTests extends ShellTestCase {
-    @Mock private CaptionWindowDecoration mCaptionWindowDecoration;
 
+    private static final String TAG = "CaptionWindowDecorViewModelTests";
+
+    @Mock private CaptionWindowDecoration mCaptionWindowDecoration;
     @Mock private CaptionWindowDecoration.Factory mCaptionWindowDecorFactory;
 
     @Mock private Handler mMainHandler;
-
     @Mock private Choreographer mMainChoreographer;
-
     @Mock private ShellTaskOrganizer mTaskOrganizer;
-
     @Mock private DisplayController mDisplayController;
-
     @Mock private SyncTransactionQueue mSyncQueue;
-
     @Mock private DesktopModeController mDesktopModeController;
-
     @Mock private InputMonitor mInputMonitor;
-
-    @Mock private InputChannel mInputChannel;
-
-    @Mock private CaptionWindowDecorViewModel.EventReceiverFactory mEventReceiverFactory;
-
-    @Mock private CaptionWindowDecorViewModel.EventReceiver mEventReceiver;
-
     @Mock private InputManager mInputManager;
 
+    @Mock private CaptionWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory;
     private final List<InputManager> mMockInputManagers = new ArrayList<>();
 
     private CaptionWindowDecorViewModel mCaptionWindowDecorViewModel;
@@ -104,44 +96,46 @@
                 mSyncQueue,
                 Optional.of(mDesktopModeController),
                 mCaptionWindowDecorFactory,
-                new MockObjectSupplier<>(mMockInputManagers, () -> mock(InputManager.class)));
-        mCaptionWindowDecorViewModel.setEventReceiverFactory(mEventReceiverFactory);
+                mMockInputMonitorFactory
+            );
 
         doReturn(mCaptionWindowDecoration)
             .when(mCaptionWindowDecorFactory)
             .create(any(), any(), any(), any(), any(), any(), any(), any());
 
-        when(mInputManager.monitorGestureInput(any(), anyInt())).thenReturn(mInputMonitor);
-        when(mEventReceiverFactory.create(any(), any(), any())).thenReturn(mEventReceiver);
-        when(mInputMonitor.getInputChannel()).thenReturn(mInputChannel);
+        when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor);
+        // InputChannel cannot be mocked because it passes to InputEventReceiver.
+        final InputChannel[] inputChannels = InputChannel.openInputChannelPair(TAG);
+        inputChannels[0].dispose();
+        when(mInputMonitor.getInputChannel()).thenReturn(inputChannels[1]);
     }
 
     @Test
     public void testDeleteCaptionOnChangeTransitionWhenNecessary() throws Exception {
-        Looper.prepare();
         final int taskId = 1;
         final ActivityManager.RunningTaskInfo taskInfo =
-                createTaskInfo(taskId, WINDOWING_MODE_FREEFORM);
+                createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM);
         SurfaceControl surfaceControl = mock(SurfaceControl.class);
-        final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
-        final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
-        GrantPermissionRule.grant(android.Manifest.permission.MONITOR_INPUT);
+        runOnMainThread(() -> {
+            final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+            final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
 
-        mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT);
+            mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT);
+
+            taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+            taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
+            mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
+        });
         verify(mCaptionWindowDecorFactory)
                 .create(
-                    mContext,
-                    mDisplayController,
-                    mTaskOrganizer,
-                    taskInfo,
-                    surfaceControl,
-                    mMainHandler,
-                    mMainChoreographer,
-                    mSyncQueue);
-
-        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED);
-        taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
-        mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
+                        mContext,
+                        mDisplayController,
+                        mTaskOrganizer,
+                        taskInfo,
+                        surfaceControl,
+                        mMainHandler,
+                        mMainChoreographer,
+                        mSyncQueue);
         verify(mCaptionWindowDecoration).close();
     }
 
@@ -149,70 +143,105 @@
     public void testCreateCaptionOnChangeTransitionWhenNecessary() throws Exception {
         final int taskId = 1;
         final ActivityManager.RunningTaskInfo taskInfo =
-                createTaskInfo(taskId, WINDOWING_MODE_UNDEFINED);
+                createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_UNDEFINED);
         SurfaceControl surfaceControl = mock(SurfaceControl.class);
-        final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
-        final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
-        taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
+        runOnMainThread(() -> {
+            final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+            final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+            taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
 
-        mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
+            mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
 
-        verify(mCaptionWindowDecorFactory, never())
+            taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+            taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
+
+            mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
+        });
+        verify(mCaptionWindowDecorFactory, times(1))
                 .create(
-                    mContext,
-                    mDisplayController,
-                    mTaskOrganizer,
-                    taskInfo,
-                    surfaceControl,
-                    mMainHandler,
-                    mMainChoreographer,
-                    mSyncQueue);
-
-        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
-        taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
-
-        mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
-
-        verify(mCaptionWindowDecorFactory)
-                .create(
-                    mContext,
-                    mDisplayController,
-                    mTaskOrganizer,
-                    taskInfo,
-                    surfaceControl,
-                    mMainHandler,
-                    mMainChoreographer,
-                    mSyncQueue);
+                        mContext,
+                        mDisplayController,
+                        mTaskOrganizer,
+                        taskInfo,
+                        surfaceControl,
+                        mMainHandler,
+                        mMainChoreographer,
+                        mSyncQueue);
     }
 
-    private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
+    @Test
+    public void testCreateAndDisposeEventReceiver() throws Exception {
+        final int taskId = 1;
+        final ActivityManager.RunningTaskInfo taskInfo =
+                createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM);
+        taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
+        runOnMainThread(() -> {
+            SurfaceControl surfaceControl = mock(SurfaceControl.class);
+            final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+            final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+
+            mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT);
+
+            mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo);
+        });
+        verify(mMockInputMonitorFactory).create(any(), any());
+        verify(mInputMonitor).dispose();
+    }
+
+    @Test
+    public void testEventReceiversOnMultipleDisplays() throws Exception {
+        runOnMainThread(() -> {
+            SurfaceView surfaceView = new SurfaceView(mContext);
+            final DisplayManager mDm = mContext.getSystemService(DisplayManager.class);
+            final VirtualDisplay secondaryDisplay = mDm.createVirtualDisplay(
+                    "testEventReceiversOnMultipleDisplays", /*width=*/ 400, /*height=*/ 400,
+                    /*densityDpi=*/ 320, surfaceView.getHolder().getSurface(),
+                    DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
+            int secondaryDisplayId = secondaryDisplay.getDisplay().getDisplayId();
+
+            final int taskId = 1;
+            final ActivityManager.RunningTaskInfo taskInfo =
+                    createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM);
+            final ActivityManager.RunningTaskInfo secondTaskInfo =
+                    createTaskInfo(taskId + 1, secondaryDisplayId, WINDOWING_MODE_FREEFORM);
+            final ActivityManager.RunningTaskInfo thirdTaskInfo =
+                    createTaskInfo(taskId + 2, secondaryDisplayId, WINDOWING_MODE_FREEFORM);
+
+            SurfaceControl surfaceControl = mock(SurfaceControl.class);
+            final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+            final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+
+            mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT);
+            mCaptionWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl,
+                    startT, finishT);
+            mCaptionWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl,
+                    startT, finishT);
+            mCaptionWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo);
+            mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo);
+        });
+        verify(mMockInputMonitorFactory, times(2)).create(any(), any());
+        verify(mInputMonitor, times(1)).dispose();
+    }
+
+    private void runOnMainThread(Runnable r) throws Exception {
+        final Handler mainHandler = new Handler(Looper.getMainLooper());
+        final CountDownLatch latch = new CountDownLatch(1);
+        mainHandler.post(() -> {
+            r.run();
+            latch.countDown();
+        });
+        latch.await(20, TimeUnit.MILLISECONDS);
+    }
+
+    private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId,
+            int displayId, int windowingMode) {
         ActivityManager.RunningTaskInfo taskInfo =
                  new TestRunningTaskInfoBuilder()
-                .setDisplayId(Display.DEFAULT_DISPLAY)
+                .setDisplayId(displayId)
                 .setVisible(true)
                 .build();
         taskInfo.taskId = taskId;
         taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
         return taskInfo;
     }
-
-    private static class MockObjectSupplier<T> implements Supplier<T> {
-        private final List<T> mObjects;
-        private final Supplier<T> mDefaultSupplier;
-        private int mNumOfCalls = 0;
-
-        private MockObjectSupplier(List<T> objects, Supplier<T> defaultSupplier) {
-            mObjects = objects;
-            mDefaultSupplier = defaultSupplier;
-        }
-
-        @Override
-        public T get() {
-            final T mock =
-                    mNumOfCalls < mObjects.size() ? mObjects.get(mNumOfCalls)
-                        : mDefaultSupplier.get();
-            ++mNumOfCalls;
-            return mock;
-        }
-    }
 }
diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java
index ccd4ed0..4d3f05b 100644
--- a/media/java/android/media/AudioFormat.java
+++ b/media/java/android/media/AudioFormat.java
@@ -373,6 +373,8 @@
      * Use {@link #ENCODING_DTS_UHD_P2} to transmit DTS UHD Profile 2 (aka DTS:X Profile 2)
      * bitstream. */
     public static final int ENCODING_DTS_UHD_P2 = 30;
+    /** Audio data format: Direct Stream Digital */
+    public static final int ENCODING_DSD = 31;
 
     /** @hide */
     public static String toLogFriendlyEncoding(int enc) {
@@ -437,6 +439,8 @@
                 return "ENCODING_DTS_HD_MA";
             case ENCODING_DTS_UHD_P2:
                 return "ENCODING_DTS_UHD_P2";
+            case ENCODING_DSD:
+                return "ENCODING_DSD";
             default :
                 return "invalid encoding " + enc;
         }
@@ -798,6 +802,7 @@
             case ENCODING_DRA:
             case ENCODING_DTS_HD_MA:
             case ENCODING_DTS_UHD_P2:
+            case ENCODING_DSD:
                 return true;
             default:
                 return false;
@@ -837,6 +842,7 @@
             case ENCODING_DRA:
             case ENCODING_DTS_HD_MA:
             case ENCODING_DTS_UHD_P2:
+            case ENCODING_DSD:
                 return true;
             default:
                 return false;
@@ -1211,6 +1217,7 @@
                 case ENCODING_DRA:
                 case ENCODING_DTS_HD_MA:
                 case ENCODING_DTS_UHD_P2:
+                case ENCODING_DSD:
                     mEncoding = encoding;
                     break;
                 case ENCODING_INVALID:
@@ -1441,7 +1448,8 @@
         ENCODING_DTS_UHD_P1,
         ENCODING_DRA,
         ENCODING_DTS_HD_MA,
-        ENCODING_DTS_UHD_P2 }
+        ENCODING_DTS_UHD_P2,
+        ENCODING_DSD }
     )
     @Retention(RetentionPolicy.SOURCE)
     public @interface Encoding {}
diff --git a/media/java/android/media/AudioProfile.java b/media/java/android/media/AudioProfile.java
index 5c5f837..356b765 100644
--- a/media/java/android/media/AudioProfile.java
+++ b/media/java/android/media/AudioProfile.java
@@ -46,11 +46,17 @@
      * Encapsulation format is defined in standard IEC 61937.
      */
     public static final int AUDIO_ENCAPSULATION_TYPE_IEC61937 = 1;
+    /**
+     * Encapsulation format is PCM, which can be used by other formats that can be wrapped in
+     * a PCM frame, such as DSD(Direct Stream Digital).
+     */
+    public static final int AUDIO_ENCAPSULATION_TYPE_PCM = 2;
 
     /** @hide */
     @IntDef({
             AUDIO_ENCAPSULATION_TYPE_NONE,
             AUDIO_ENCAPSULATION_TYPE_IEC61937,
+            AUDIO_ENCAPSULATION_TYPE_PCM,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EncapsulationType {}
@@ -122,6 +128,7 @@
      *
      * @see #AUDIO_ENCAPSULATION_TYPE_NONE
      * @see #AUDIO_ENCAPSULATION_TYPE_IEC61937
+     * @see #AUDIO_ENCAPSULATION_TYPE_PCM
      */
     public @EncapsulationType int getEncapsulationType() {
         return mEncapsulationType;
diff --git a/media/java/android/media/audio/common/AidlConversion.java b/media/java/android/media/audio/common/AidlConversion.java
index 4cf3b3e..490809c 100644
--- a/media/java/android/media/audio/common/AidlConversion.java
+++ b/media/java/android/media/audio/common/AidlConversion.java
@@ -614,6 +614,8 @@
         switch (type) {
             case android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_IEC61937:
                 return AudioEncapsulationType.IEC61937;
+            case android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_PCM:
+                return AudioEncapsulationType.PCM;
             case android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE:
             default:
                 return AudioEncapsulationType.NONE;
@@ -629,6 +631,8 @@
         switch (type) {
             case AudioEncapsulationType.IEC61937:
                 return android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_IEC61937;
+            case AudioEncapsulationType.PCM:
+                return android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_PCM;
             case AudioEncapsulationType.NONE:
             default:
                 return android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE;
diff --git a/media/java/android/media/projection/IMediaProjectionCallback.aidl b/media/java/android/media/projection/IMediaProjectionCallback.aidl
index f3743d1..2c8de2e 100644
--- a/media/java/android/media/projection/IMediaProjectionCallback.aidl
+++ b/media/java/android/media/projection/IMediaProjectionCallback.aidl
@@ -19,4 +19,5 @@
 /** {@hide} */
 oneway interface IMediaProjectionCallback {
     void onStop();
+    void onCapturedContentResize(int width, int height);
 }
diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl
index 1d58a40..a63d02b 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -27,12 +27,32 @@
 interface IMediaProjectionManager {
     @UnsupportedAppUsage
     boolean hasProjectionPermission(int uid, String packageName);
+
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
     IMediaProjection createProjection(int uid, String packageName, int type,
             boolean permanentGrant);
+
     boolean isValidMediaProjection(IMediaProjection projection);
+
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
     MediaProjectionInfo getActiveProjectionInfo();
+
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
     void stopActiveProjection();
+
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
+    void notifyActiveProjectionCapturedContentResized(int width, int height);
+
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
     void addCallback(IMediaProjectionWatcherCallback callback);
+
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
     void removeCallback(IMediaProjectionWatcherCallback callback);
 
     /**
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index ae44fc5..3dfff1f 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -234,7 +234,7 @@
     /**
      * Callbacks for the projection session.
      */
-    public static abstract class Callback {
+    public abstract static class Callback {
         /**
          * Called when the MediaProjection session is no longer valid.
          * <p>
@@ -243,6 +243,46 @@
          * </p>
          */
         public void onStop() { }
+
+        /**
+         * Indicates the width and height of the captured region in pixels. Called immediately after
+         * capture begins to provide the app with accurate sizing for the stream. Also called
+         * when the region captured in this MediaProjection session is resized.
+         * <p>
+         * The given width and height, in pixels, corresponds to the same width and height that
+         * would be returned from {@link android.view.WindowMetrics#getBounds()}
+         * </p>
+         * <p>
+         * Without the application resizing the {@link VirtualDisplay} (returned from
+         * {@code MediaProjection#createVirtualDisplay}) and output {@link Surface} (provided
+         * to {@code MediaProjection#createVirtualDisplay}), the captured stream will have
+         * letterboxing (black bars) around the recorded content to make up for the
+         * difference in aspect ratio.
+         * </p>
+         * <p>
+         * The application can prevent the letterboxing by overriding this method, and
+         * updating the size of both the {@link VirtualDisplay} and output {@link Surface}:
+         * </p>
+         *
+         * <pre>
+         * &#x40;Override
+         * public String onCapturedContentResize(int width, int height) {
+         *     // VirtualDisplay instance from MediaProjection#createVirtualDisplay
+         *     virtualDisplay.resize(width, height, dpi);
+         *
+         *     // Create a new Surface with the updated size (depending on the application's use
+         *     // case, this may be through different APIs - see Surface documentation for
+         *     // options).
+         *     int texName; // the OpenGL texture object name
+         *     SurfaceTexture surfaceTexture = new SurfaceTexture(texName);
+         *     surfaceTexture.setDefaultBufferSize(width, height);
+         *     Surface surface = new Surface(surfaceTexture);
+         *
+         *     // Ensure the VirtualDisplay has the updated Surface to send the capture to.
+         *     virtualDisplay.setSurface(surface);
+         * }</pre>
+         */
+        public void onCapturedContentResize(int width, int height) { }
     }
 
     private final class MediaProjectionCallback extends IMediaProjectionCallback.Stub {
@@ -252,6 +292,13 @@
                 cbr.onStop();
             }
         }
+
+        @Override
+        public void onCapturedContentResize(int width, int height) {
+            for (CallbackRecord cbr : mCallbacks.values()) {
+                cbr.onCapturedContentResize(width, height);
+            }
+        }
     }
 
     private final static class CallbackRecord {
@@ -271,5 +318,9 @@
                 }
             });
         }
+
+        public void onCapturedContentResize(int width, int height) {
+            mHandler.post(() -> mCallback.onCapturedContentResize(width, height));
+        }
     }
 }
diff --git a/media/java/android/media/projection/MediaProjectionConfig.aidl b/media/java/android/media/projection/MediaProjectionConfig.aidl
new file mode 100644
index 0000000..f78385f
--- /dev/null
+++ b/media/java/android/media/projection/MediaProjectionConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.projection;
+
+parcelable MediaProjectionConfig;
diff --git a/media/java/android/media/projection/MediaProjectionConfig.java b/media/java/android/media/projection/MediaProjectionConfig.java
new file mode 100644
index 0000000..29afaa6
--- /dev/null
+++ b/media/java/android/media/projection/MediaProjectionConfig.java
@@ -0,0 +1,293 @@
+/*
+ * 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.projection;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+import com.android.internal.util.DataClass;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Configure the {@link MediaProjection} session requested from
+ * {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}.
+ */
+@DataClass(
+        genEqualsHashCode = true,
+        genAidl = true,
+        genSetters = false,
+        genConstructor = false,
+        genBuilder = false,
+        genToString = false,
+        genHiddenConstDefs = true,
+        genHiddenGetters = true,
+        genConstDefs = false
+)
+public final class MediaProjectionConfig implements Parcelable {
+
+    /**
+     * The user, rather than the host app, determines which region of the display to capture.
+     * @hide
+     */
+    public static final int CAPTURE_REGION_USER_CHOICE = 0;
+
+    /**
+     * The host app specifies a particular display to capture.
+     * @hide
+     */
+    public static final int CAPTURE_REGION_FIXED_DISPLAY = 1;
+
+    /** @hide */
+    @IntDef(prefix = "CAPTURE_REGION_", value = {
+            CAPTURE_REGION_USER_CHOICE,
+            CAPTURE_REGION_FIXED_DISPLAY
+    })
+    @Retention(SOURCE)
+    public @interface CaptureRegion {
+    }
+
+    /**
+     * The particular display to capture. Only used when {@link #getRegionToCapture()} is
+     * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise.
+     *
+     * Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}.
+     */
+    @IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY)
+    private int mDisplayToCapture;
+
+    /**
+     * The region to capture. Defaults to the user's choice.
+     */
+    @CaptureRegion
+    private int mRegionToCapture = CAPTURE_REGION_USER_CHOICE;
+
+    /**
+     * Default instance, with region set to the user's choice.
+     */
+    private MediaProjectionConfig() {
+    }
+
+    /**
+     * Customized instance, with region set to the provided value.
+     */
+    private MediaProjectionConfig(@CaptureRegion int captureRegion) {
+        mRegionToCapture = captureRegion;
+    }
+
+    /**
+     * Returns an instance which restricts the user to capturing a particular display.
+     *
+     * @param displayId The id of the display to capture. Only supports values of
+     *                  {@link android.view.Display#DEFAULT_DISPLAY}.
+     * @throws IllegalArgumentException If the given {@code displayId} is outside the range of
+     * supported values.
+     */
+    @NonNull
+    public static MediaProjectionConfig createConfigForDisplay(
+            @IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY) int displayId) {
+        if (displayId != DEFAULT_DISPLAY) {
+            throw new IllegalArgumentException(
+                    "A config for capturing the non-default display is not supported; requested "
+                            + "display id "
+                            + displayId);
+        }
+        MediaProjectionConfig config = new MediaProjectionConfig(CAPTURE_REGION_FIXED_DISPLAY);
+        config.mDisplayToCapture = displayId;
+        return config;
+    }
+
+    /**
+     * Returns an instance which allows the user to decide which region is captured. The consent
+     * dialog presents the user with all possible options. If the user selects display capture,
+     * then only the {@link android.view.Display#DEFAULT_DISPLAY} is supported.
+     *
+     * <p>
+     * When passed in to
+     * {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}, the consent
+     * dialog shown to the user will be the same as if just
+     * {@link MediaProjectionManager#createScreenCaptureIntent()} was invoked.
+     * </p>
+     */
+    @NonNull
+    public static MediaProjectionConfig createConfigForUserChoice() {
+        return new MediaProjectionConfig(CAPTURE_REGION_USER_CHOICE);
+    }
+
+    /**
+     * Returns string representation of the captured region.
+     */
+    @NonNull
+    private static String captureRegionToString(int value) {
+        switch (value) {
+            case CAPTURE_REGION_USER_CHOICE:
+                return "CAPTURE_REGION_USERS_CHOICE";
+            case CAPTURE_REGION_FIXED_DISPLAY:
+                return "CAPTURE_REGION_GIVEN_DISPLAY";
+            default:
+                return Integer.toHexString(value);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "MediaProjectionConfig { "
+                + "displayToCapture = " + mDisplayToCapture + ", "
+                + "regionToCapture = " + captureRegionToString(mRegionToCapture)
+                + " }";
+    }
+
+
+
+
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/media/java/android/media/projection/MediaProjectionConfig.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    /**
+     * The particular display to capture. Only used when {@link #getRegionToCapture()} is
+     * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise.
+     *
+     * Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}.
+     *
+     * @hide
+     */
+    @DataClass.Generated.Member
+    public @IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY) int getDisplayToCapture() {
+        return mDisplayToCapture;
+    }
+
+    /**
+     * The region to capture. Defaults to the user's choice.
+     *
+     * @hide
+     */
+    @DataClass.Generated.Member
+    public @CaptureRegion int getRegionToCapture() {
+        return mRegionToCapture;
+    }
+
+    @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(MediaProjectionConfig other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        MediaProjectionConfig that = (MediaProjectionConfig) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && mDisplayToCapture == that.mDisplayToCapture
+                && mRegionToCapture == that.mRegionToCapture;
+    }
+
+    @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 + mDisplayToCapture;
+        _hash = 31 * _hash + mRegionToCapture;
+        return _hash;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        dest.writeInt(mDisplayToCapture);
+        dest.writeInt(mRegionToCapture);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ MediaProjectionConfig(@NonNull android.os.Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        int displayToCapture = in.readInt();
+        int regionToCapture = in.readInt();
+
+        this.mDisplayToCapture = displayToCapture;
+        AnnotationValidations.validate(
+                IntRange.class, null, mDisplayToCapture,
+                "from", DEFAULT_DISPLAY,
+                "to", DEFAULT_DISPLAY);
+        this.mRegionToCapture = regionToCapture;
+        AnnotationValidations.validate(
+                CaptureRegion.class, null, mRegionToCapture);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<MediaProjectionConfig> CREATOR
+            = new Parcelable.Creator<MediaProjectionConfig>() {
+        @Override
+        public MediaProjectionConfig[] newArray(int size) {
+            return new MediaProjectionConfig[size];
+        }
+
+        @Override
+        public MediaProjectionConfig createFromParcel(@NonNull android.os.Parcel in) {
+            return new MediaProjectionConfig(in);
+        }
+    };
+
+    @DataClass.Generated(
+            time = 1671030124845L,
+            codegenVersion = "1.0.23",
+            sourceFile = "frameworks/base/media/java/android/media/projection/MediaProjectionConfig.java",
+            inputSignatures = "public static final  int CAPTURE_REGION_USER_CHOICE\npublic static final  int CAPTURE_REGION_FIXED_DISPLAY\nprivate @android.annotation.IntRange int mDisplayToCapture\nprivate @android.media.projection.MediaProjectionConfig.CaptureRegion int mRegionToCapture\npublic static @android.annotation.NonNull android.media.projection.MediaProjectionConfig createConfigForDisplay(int)\npublic static @android.annotation.NonNull android.media.projection.MediaProjectionConfig createConfigForUserChoice()\nprivate static @android.annotation.NonNull java.lang.String captureRegionToString(int)\npublic @java.lang.Override java.lang.String toString()\nclass MediaProjectionConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genAidl=true, genSetters=false, genConstructor=false, genBuilder=false, genToString=false, genHiddenConstDefs=true, genHiddenGetters=true, genConstDefs=false)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index b3bd980..a4215e68 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -38,6 +38,13 @@
 @SystemService(Context.MEDIA_PROJECTION_SERVICE)
 public final class MediaProjectionManager {
     private static final String TAG = "MediaProjectionManager";
+
+    /**
+     * Intent extra to customize the permission dialog based on the host app's preferences.
+     * @hide
+     */
+    public static final String EXTRA_MEDIA_PROJECTION_CONFIG =
+            "android.media.projection.extra.EXTRA_MEDIA_PROJECTION_CONFIG";
     /** @hide */
     public static final String EXTRA_APP_TOKEN = "android.media.projection.extra.EXTRA_APP_TOKEN";
     /** @hide */
@@ -64,11 +71,13 @@
     }
 
     /**
-     * Returns an Intent that <b>must</b> be passed to startActivityForResult()
-     * in order to start screen capture. The activity will prompt
-     * the user whether to allow screen capture.  The result of this
-     * activity should be passed to getMediaProjection.
+     * Returns an {@link Intent} that <b>must</b> be passed to
+     * {@link Activity#startActivityForResult(Intent, int)} (or similar) in order to start screen
+     * capture. The activity will prompt the user whether to allow screen capture.  The result of
+     * this activity (received by overriding {@link Activity#onActivityResult(int, int, Intent)})
+     * should be passed to {@link #getMediaProjection(int, Intent)}.
      */
+    @NonNull
     public Intent createScreenCaptureIntent() {
         Intent i = new Intent();
         final ComponentName mediaProjectionPermissionDialogComponent =
@@ -80,6 +89,49 @@
     }
 
     /**
+     * Returns an {@link Intent} that <b>must</b> be passed to
+     * {@link Activity#startActivityForResult(Intent, int)} (or similar) in order to start screen
+     * capture. Customizes the activity and resulting {@link MediaProjection} session based up
+     * the provided {@code config}. The activity will prompt the user whether to allow screen
+     * capture. The result of this activity (received by overriding
+     * {@link Activity#onActivityResult(int, int, Intent)}) should be passed to
+     * {@link #getMediaProjection(int, Intent)}.
+     *
+     * <p>
+     * If {@link MediaProjectionConfig} was created from:
+     * <li>
+     *     <ul>
+     *         {@link MediaProjectionConfig#createConfigForDisplay(int)}, then creates an
+     *         {@link Intent} for capturing this particular display. The activity limits the user's
+     *         choice to just the display specified.
+     *     </ul>
+     *     <ul>
+     *         {@link MediaProjectionConfig#createConfigForUserChoice()}, then creates an
+     *         {@link Intent} for deferring which region to capture to the user. This gives the
+     *         user the same behaviour as calling {@link #createScreenCaptureIntent()}. The
+     *         activity gives the user the choice between
+     *         {@link android.view.Display#DEFAULT_DISPLAY}, or a different region.
+     *     </ul>
+     * </li>
+     *
+     * @param config Customization for the {@link MediaProjection} that this {@link Intent} requests
+     *               the user's consent for.
+     * @return An {@link Intent} requesting the user's consent, specialized based upon the given
+     * configuration.
+     */
+    @NonNull
+    public Intent createScreenCaptureIntent(@NonNull MediaProjectionConfig config) {
+        Intent i = new Intent();
+        final ComponentName mediaProjectionPermissionDialogComponent =
+                ComponentName.unflattenFromString(mContext.getResources()
+                        .getString(com.android.internal.R.string
+                                .config_mediaProjectionPermissionDialogComponent));
+        i.setComponent(mediaProjectionPermissionDialogComponent);
+        i.putExtra(EXTRA_MEDIA_PROJECTION_CONFIG, config);
+        return i;
+    }
+
+    /**
      * Retrieves the {@link MediaProjection} obtained from a successful screen
      * capture request. The result code and data from the request are provided
      * by overriding {@link Activity#onActivityResult(int, int, Intent)
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 90eed9e..00150d5 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -457,7 +457,9 @@
         /**
          * Receives started recording's ID.
          *
-         * @param recordingId The ID of the recording started
+         * @param recordingId The ID of the recording started. The TV app should provide and
+         *                    maintain this ID to identify the recording in the future.
+         * @see #onRecordingStopped(String)
          */
         public void onRecordingStarted(@NonNull String recordingId) {
         }
@@ -465,13 +467,13 @@
         /**
          * Receives stopped recording's ID.
          *
-         * @param recordingId The ID of the recording stopped
-         * @hide
+         * @param recordingId The ID of the recording stopped. This ID is created and maintained by
+         *                    the TV app when the recording was started.
+         * @see #onRecordingStarted(String)
          */
         public void onRecordingStopped(@NonNull String recordingId) {
         }
 
-
         /**
          * Receives signing result.
          * @param signingId the ID to identify the request. It's the same as the corresponding ID in
@@ -952,13 +954,14 @@
         }
 
         /**
-         * Requests starting of recording
+         * Requests the recording associated with the recordingId to stop.
          *
-         * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to
+         * <p> This is used to request the associated {@link android.media.tv.TvRecordingClient} to
          * call {@link android.media.tv.TvRecordingClient#stopRecording()}.
-         * @see android.media.tv.TvRecordingClient#stopRecording()
          *
-         * @hide
+         * @param recordingId The ID of the recording to stop. This is provided by the TV app in
+         *                    {@link TvInteractiveAppView#notifyRecordingStarted(String)}
+         * @see android.media.tv.TvRecordingClient#stopRecording()
          */
         @CallSuper
         public void requestStopRecording(@NonNull String recordingId) {
@@ -976,8 +979,6 @@
             });
         }
 
-
-
         /**
          * Requests signing of the given data.
          *
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index fcd781b..1177688 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -585,6 +585,7 @@
      *
      * @param recordingId The ID of the recording started. This ID is created and maintained by the
      *                    TV app and is used to identify the recording in the future.
+     * @see TvInteractiveAppView#notifyRecordingStopped(String)
      */
     public void notifyRecordingStarted(@NonNull String recordingId) {
         if (DEBUG) {
@@ -601,7 +602,6 @@
      * @param recordingId The ID of the recording stopped. This ID is created and maintained
      *                    by the TV app when a recording is started.
      * @see TvInteractiveAppView#notifyRecordingStarted(String)
-     * @hide
      */
     public void notifyRecordingStopped(@NonNull String recordingId) {
         if (DEBUG) {
@@ -877,7 +877,8 @@
          * is called.
          *
          * @param iAppServiceId The ID of the TV interactive app service bound to this view.
-         * @param programUri The program URI to record
+         * @param programUri The URI of the program to record
+         *
          */
         public void onRequestStartRecording(
                 @NonNull String iAppServiceId,
@@ -885,12 +886,14 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#requestStopRecording()}
+         * This is called when {@link TvInteractiveAppService.Session#requestStopRecording(String)}
          * is called.
          *
          * @param iAppServiceId The ID of the TV interactive app service bound to this view.
-         * @param recordingId The ID of the recording to stop.
-         * @hide
+         * @param recordingId The ID of the recording to stop. This is provided by the TV app in
+         *                    {@link #notifyRecordingStarted(String)}
+         * @see #notifyRecordingStarted(String)
+         * @see #notifyRecordingStopped(String)
          */
         public void onRequestStopRecording(
                 @NonNull String iAppServiceId,
diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp
new file mode 100644
index 0000000..08d9501
--- /dev/null
+++ b/media/tests/projection/Android.bp
@@ -0,0 +1,46 @@
+//########################################################################
+// Build MediaProjectionTests package
+//########################################################################
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "MediaProjectionTests",
+
+    srcs: ["**/*.java"],
+
+    libs: [
+        "android.test.base",
+        "android.test.mock",
+        "android.test.runner",
+    ],
+
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "mockito-target-extended-minus-junit4",
+        "platform-test-annotations",
+        "testng",
+        "truth-prebuilt",
+    ],
+
+    // Needed for mockito-target-extended-minus-junit4
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+
+    test_suites: ["device-tests"],
+
+    platform_apis: true,
+
+    certificate: "platform",
+}
diff --git a/media/tests/projection/AndroidManifest.xml b/media/tests/projection/AndroidManifest.xml
new file mode 100644
index 0000000..62f148c
--- /dev/null
+++ b/media/tests/projection/AndroidManifest.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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          android:installLocation="internalOnly"
+          package="android.media.projection.mediaprojectiontests"
+          android:sharedUserId="com.android.uid.test">
+    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
+
+    <application android:debuggable="true"
+                 android:testOnly="true">
+        <uses-library android:name="android.test.mock" android:required="true"/>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.media.projection.mediaprojectiontests"
+                     android:label="MediaProjection package tests"/>
+</manifest>
diff --git a/media/tests/projection/AndroidTest.xml b/media/tests/projection/AndroidTest.xml
new file mode 100644
index 0000000..f64930a
--- /dev/null
+++ b/media/tests/projection/AndroidTest.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.
+  -->
+
+<configuration description="Runs MediaProjection package Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="MediaProjectionTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="MediaProjectionTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.media.projection.mediaprojectiontests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false" />
+    </test>
+</configuration>
diff --git a/media/tests/projection/TEST_MAPPING b/media/tests/projection/TEST_MAPPING
new file mode 100644
index 0000000..ddb68af
--- /dev/null
+++ b/media/tests/projection/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+    "presubmit": [
+        {
+            "name": "FrameworksServicesTests",
+            "options": [
+                {"include-filter": "android.media.projection.mediaprojectiontests"},
+                {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+                {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+                {"exclude-annotation": "org.junit.Ignore"}
+            ]
+        }
+    ]
+}
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java
new file mode 100644
index 0000000..a30f2e3
--- /dev/null
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.projection;
+
+import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY;
+import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_USER_CHOICE;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the {@link MediaProjectionConfig} class.
+ *
+ * Build/Install/Run:
+ * atest MediaProjectionTests:MediaProjectionConfigTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class MediaProjectionConfigTest {
+    private static final MediaProjectionConfig DISPLAY_CONFIG =
+            MediaProjectionConfig.createConfigForDisplay(DEFAULT_DISPLAY);
+    private static final MediaProjectionConfig USERS_CHOICE_CONFIG =
+            MediaProjectionConfig.createConfigForUserChoice();
+
+    @Test
+    public void testParcelable() {
+        Parcel parcel = Parcel.obtain();
+        DISPLAY_CONFIG.writeToParcel(parcel, 0 /* flags */);
+        parcel.setDataPosition(0);
+        MediaProjectionConfig config = MediaProjectionConfig.CREATOR.createFromParcel(parcel);
+        assertThat(DISPLAY_CONFIG).isEqualTo(config);
+        parcel.recycle();
+    }
+
+    @Test
+    public void testCreateDisplayConfig() {
+        assertThrows(IllegalArgumentException.class,
+                () -> MediaProjectionConfig.createConfigForDisplay(-1));
+        assertThrows(IllegalArgumentException.class,
+                () -> MediaProjectionConfig.createConfigForDisplay(DEFAULT_DISPLAY + 1));
+        assertThat(DISPLAY_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_FIXED_DISPLAY);
+        assertThat(DISPLAY_CONFIG.getDisplayToCapture()).isEqualTo(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public void testCreateUsersChoiceConfig() {
+        assertThat(USERS_CHOICE_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_USER_CHOICE);
+    }
+
+    @Test
+    public void testEquals() {
+        assertThat(MediaProjectionConfig.createConfigForUserChoice()).isEqualTo(
+                USERS_CHOICE_CONFIG);
+        assertThat(DISPLAY_CONFIG).isNotEqualTo(USERS_CHOICE_CONFIG);
+        assertThat(MediaProjectionConfig.createConfigForDisplay(DEFAULT_DISPLAY)).isEqualTo(
+                DISPLAY_CONFIG);
+    }
+}
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionManagerTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionManagerTest.java
new file mode 100644
index 0000000..a3e4908
--- /dev/null
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionManagerTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.projection;
+
+import static android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION_CONFIG;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.quality.Strictness.LENIENT;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoSession;
+
+/**
+ * Tests for the {@link MediaProjectionManager} class.
+ *
+ * Build/Install/Run:
+ * atest MediaProjectionTests:MediaProjectionManagerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class MediaProjectionManagerTest {
+    private MediaProjectionManager mMediaProjectionManager;
+    private Context mContext;
+    private MockitoSession mMockingSession;
+    private static final MediaProjectionConfig DISPLAY_CONFIG =
+            MediaProjectionConfig.createConfigForDisplay(DEFAULT_DISPLAY);
+    private static final MediaProjectionConfig USERS_CHOICE_CONFIG =
+            MediaProjectionConfig.createConfigForUserChoice();
+
+    @Before
+    public void setup() throws Exception {
+        mMockingSession =
+                mockitoSession()
+                        .initMocks(this)
+                        .strictness(LENIENT)
+                        .startMocking();
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        spyOn(mContext);
+        mMediaProjectionManager = new MediaProjectionManager(mContext);
+    }
+
+    @After
+    public void teardown() {
+        mMockingSession.finishMocking();
+    }
+
+    @Test
+    public void testCreateScreenCaptureIntent() {
+        final String dialogPackage = "test.package";
+        preparePermissionDialogComponent(dialogPackage);
+
+        final Intent intent = mMediaProjectionManager.createScreenCaptureIntent();
+        assertThat(intent).isNotNull();
+        assertThat(intent.getComponent().getPackageName()).contains(dialogPackage);
+    }
+
+    @Test
+    public void testCreateScreenCaptureIntent_display() {
+        final String dialogPackage = "test.package";
+        preparePermissionDialogComponent(dialogPackage);
+
+        final Intent intent = mMediaProjectionManager.createScreenCaptureIntent(DISPLAY_CONFIG);
+        assertThat(intent).isNotNull();
+        assertThat(intent.getComponent().getPackageName()).contains(dialogPackage);
+        assertThat(intent.getParcelableExtra(EXTRA_MEDIA_PROJECTION_CONFIG,
+                MediaProjectionConfig.class)).isEqualTo(DISPLAY_CONFIG);
+    }
+
+    @Test
+    public void testCreateScreenCaptureIntent_usersChoice() {
+        final String dialogPackage = "test.package";
+        preparePermissionDialogComponent(dialogPackage);
+
+        final Intent intent = mMediaProjectionManager.createScreenCaptureIntent(
+                USERS_CHOICE_CONFIG);
+        assertThat(intent).isNotNull();
+        assertThat(intent.getComponent().getPackageName()).contains(dialogPackage);
+        assertThat(intent.getParcelableExtra(EXTRA_MEDIA_PROJECTION_CONFIG,
+                MediaProjectionConfig.class)).isEqualTo(USERS_CHOICE_CONFIG);
+    }
+
+    private void preparePermissionDialogComponent(@NonNull String dialogPackage) {
+        final Resources mockResources = mock(Resources.class);
+        when(mContext.getResources()).thenReturn(mockResources);
+        doReturn(dialogPackage + "/.TestActivity").when(mockResources).getString(
+                com.android.internal.R.string
+                        .config_mediaProjectionPermissionDialogComponent);
+    }
+}
diff --git a/packages/CompanionDeviceManager/res/drawable-night/ic_permission_microphone.xml b/packages/CompanionDeviceManager/res/drawable-night/ic_permission_microphone.xml
new file mode 100644
index 0000000..161e4e6e
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/drawable-night/ic_permission_microphone.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="@android:color/system_accent1_200">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,14Q10.75,14 9.875,13.125Q9,12.25 9,11V5Q9,3.75 9.875,2.875Q10.75,2 12,2Q13.25,2 14.125,2.875Q15,3.75 15,5V11Q15,12.25 14.125,13.125Q13.25,14 12,14ZM12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8ZM11,21V17.925Q8.4,17.575 6.7,15.6Q5,13.625 5,11H7Q7,13.075 8.463,14.537Q9.925,16 12,16Q14.075,16 15.538,14.537Q17,13.075 17,11H19Q19,13.625 17.3,15.6Q15.6,17.575 13,17.925V21ZM12,12Q12.425,12 12.713,11.712Q13,11.425 13,11V5Q13,4.575 12.713,4.287Q12.425,4 12,4Q11.575,4 11.288,4.287Q11,4.575 11,5V11Q11,11.425 11.288,11.712Q11.575,12 12,12Z"/>
+</vector>
diff --git a/packages/CompanionDeviceManager/res/drawable/ic_permission_microphone.xml b/packages/CompanionDeviceManager/res/drawable/ic_permission_microphone.xml
new file mode 100644
index 0000000..eca625d
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/drawable/ic_permission_microphone.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ 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:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="@android:color/system_accent1_600">
+  <path android:fillColor="@android:color/white" android:pathData="M12,14Q10.75,14 9.875,13.125Q9,12.25 9,11V5Q9,3.75 9.875,2.875Q10.75,2 12,2Q13.25,2 14.125,2.875Q15,3.75 15,5V11Q15,12.25 14.125,13.125Q13.25,14 12,14ZM12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8ZM11,21V17.925Q8.4,17.575 6.7,15.6Q5,13.625 5,11H7Q7,13.075 8.463,14.537Q9.925,16 12,16Q14.075,16 15.538,14.537Q17,13.075 17,11H19Q19,13.625 17.3,15.6Q15.6,17.575 13,17.925V21ZM12,12Q12.425,12 12.713,11.712Q13,11.425 13,11V5Q13,4.575 12.713,4.287Q12.425,4 12,4Q11.575,4 11.288,4.287Q11,4.575 11,5V11Q11,11.425 11.288,11.712Q11.575,12 12,12Z"/>
+</vector>
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index ecc5f81..3b21541 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -140,6 +140,9 @@
     <!-- Calendar permission will be granted of corresponding profile [CHAR LIMIT=30] -->
     <string name="permission_calendar">Calendar</string>
 
+    <!-- Microphone permission will be granted to corresponding profile [CHAR LIMIT=30] -->
+    <string name="permission_microphone">Microphone</string>
+
     <!-- Nearby devices permission will be granted of corresponding profile [CHAR LIMIT=30] -->
     <string name="permission_nearby_devices">Nearby devices</string>
 
@@ -169,6 +172,10 @@
     <!-- TODO(b/253644212) Need the description for calendar permission  -->
     <string name="permission_calendar_summary"></string>
 
+    <!-- Description of microphone permission of corresponding profile [CHAR LIMIT=NONE] -->
+    <!-- TODO(b/256140614) Need the description for microphone permission  -->
+    <string name="permission_microphone_summary">Can record audio using the microphone</string>
+
     <!-- Description of nearby devices' permission of corresponding profile [CHAR LIMIT=NONE] -->
     <!-- TODO(b/253644212) Need the description for nearby devices' permission  -->
     <string name="permission_nearby_devices_summary"></string>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index c249f55..49a63345 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -36,6 +36,7 @@
 import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_APP_STREAMING;
 import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_CALENDAR;
 import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_CONTACTS;
+import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_MICROPHONE;
 import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_NEARBY_DEVICES;
 import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_NEARBY_DEVICE_STREAMING;
 import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_NOTIFICATION;
@@ -596,10 +597,9 @@
                     this, R.string.summary_glasses_single_device, profileName, appLabel);
             profileIcon = getIcon(this, R.drawable.ic_glasses);
 
-            // TODO (b/256140614): add PERMISSION_MICROPHONE
             mPermissionTypes.addAll(Arrays.asList(
                     PERMISSION_PHONE, PERMISSION_SMS, PERMISSION_CONTACTS,
-                    PERMISSION_NEARBY_DEVICES));
+                    PERMISSION_MICROPHONE, PERMISSION_NEARBY_DEVICES));
 
             setupPermissionList();
         } else {
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
index 90b94fb..00c44d6 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
@@ -51,6 +51,7 @@
     static final int PERMISSION_CALENDAR = 6;
     static final int PERMISSION_NEARBY_DEVICES = 7;
     static final int PERMISSION_NEARBY_DEVICE_STREAMING = 8;
+    static final int PERMISSION_MICROPHONE = 9;
 
     private static final Map<Integer, Integer> sTitleMap;
     static {
@@ -64,6 +65,7 @@
         map.put(PERMISSION_CALENDAR, R.string.permission_calendar);
         map.put(PERMISSION_NEARBY_DEVICES, R.string.permission_nearby_devices);
         map.put(PERMISSION_NEARBY_DEVICE_STREAMING, R.string.permission_nearby_device_streaming);
+        map.put(PERMISSION_MICROPHONE, R.string.permission_microphone);
         sTitleMap = unmodifiableMap(map);
     }
 
@@ -80,6 +82,7 @@
         map.put(PERMISSION_NEARBY_DEVICES, R.string.permission_nearby_devices_summary);
         map.put(PERMISSION_NEARBY_DEVICE_STREAMING,
                 R.string.permission_nearby_device_streaming_summary);
+        map.put(PERMISSION_MICROPHONE, R.string.permission_microphone_summary);
         sSummaryMap = unmodifiableMap(map);
     }
 
@@ -96,6 +99,7 @@
         map.put(PERMISSION_NEARBY_DEVICES, R.drawable.ic_permission_nearby_devices);
         map.put(PERMISSION_NEARBY_DEVICE_STREAMING,
                 R.drawable.ic_permission_nearby_device_streaming);
+        map.put(PERMISSION_MICROPHONE, R.drawable.ic_permission_microphone);
         sIconMap = unmodifiableMap(map);
     }
 
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 4faf00c..d2b2924 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -41,8 +41,6 @@
 import android.os.ResultReceiver
 import android.service.credentials.CredentialProviderService
 import com.android.credentialmanager.createflow.CreateCredentialUiState
-import com.android.credentialmanager.createflow.EnabledProviderInfo
-import com.android.credentialmanager.createflow.RemoteInfo
 import com.android.credentialmanager.getflow.GetCredentialUiState
 import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle
 import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
@@ -63,7 +61,7 @@
     requestInfo = intent.extras?.getParcelable(
       RequestInfo.EXTRA_REQUEST_INFO,
       RequestInfo::class.java
-    ) ?: testGetRequestInfo()
+    ) ?: testCreatePasskeyRequestInfo()
 
     providerEnabledList = when (requestInfo.type) {
       RequestInfo.TYPE_CREATE ->
@@ -101,7 +99,7 @@
   }
 
   fun onOptionSelected(
-    providerPackageName: String,
+    providerId: String,
     entryKey: String,
     entrySubkey: String,
     resultCode: Int? = null,
@@ -109,7 +107,7 @@
   ) {
     val userSelectionDialogResult = UserSelectionDialogResult(
       requestInfo.token,
-      providerPackageName,
+      providerId,
       entryKey,
       entrySubkey,
       if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
@@ -138,36 +136,15 @@
     val providerDisabledList = CreateFlowUtils.toDisabledProviderList(
       // Handle runtime cast error
       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,
-      CreateFlowUtils.toCreateScreenState(
-        createOptionSize, false,
-        requestDisplayInfo, defaultProvider, remoteEntry),
-      requestDisplayInfo,
-      false,
-      CreateFlowUtils.toActiveEntry(
-        /*defaultProvider=*/defaultProvider, createOptionSize,
-        lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
-    )
+    return CreateFlowUtils.toCreateCredentialUiState(
+      providerEnabledList, providerDisabledList, requestDisplayInfo, false)
   }
 
   companion object {
+    // TODO: find a way to resolve this static field leak problem
     lateinit var repo: CredentialManagerRepo
 
     fun setup(
@@ -198,7 +175,6 @@
         .setRemoteEntry(
           newRemoteEntry("key2", "subkey-1")
         )
-        .setIsDefaultProvider(true)
         .build(),
       CreateCredentialProviderData
         .Builder("com.dashlane")
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 6a4c599..cdff2d4 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -43,6 +43,7 @@
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     CredentialManagerRepo.setup(this, intent)
+    UserConfigRepo.setup(this)
     val requestInfo = CredentialManagerRepo.getInstance().requestInfo
     setContent {
       CredentialSelectorTheme {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 56fbf66..db676b2 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -31,6 +31,8 @@
 import com.android.credentialmanager.createflow.EnabledProviderInfo
 import com.android.credentialmanager.createflow.CreateScreenState
 import com.android.credentialmanager.createflow.ActiveEntry
+import com.android.credentialmanager.createflow.DisabledProviderInfo
+import com.android.credentialmanager.createflow.CreateCredentialUiState
 import com.android.credentialmanager.getflow.ActionEntryInfo
 import com.android.credentialmanager.getflow.AuthenticationEntryInfo
 import com.android.credentialmanager.getflow.CredentialEntryInfo
@@ -208,14 +210,13 @@
         val pkgInfo = packageManager
           .getPackageInfo(packageName!!,
             PackageManager.PackageInfoFlags.of(0))
-        com.android.credentialmanager.createflow.EnabledProviderInfo(
+        EnabledProviderInfo(
           // TODO: decide what to do when failed to load a provider icon
           icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!,
           name = it.providerFlattenedComponentName,
           displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
           createOptions = toCreationOptionInfoList(
             it.providerFlattenedComponentName, it.saveEntries, requestDisplayInfo, context),
-          isDefault = it.isDefaultProvider,
           remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry),
         )
       }
@@ -256,8 +257,7 @@
             createCredentialRequestJetpack.password,
             createCredentialRequestJetpack.type,
             requestInfo.appPackageName,
-            context.getDrawable(R.drawable.ic_password)!!,
-            requestInfo.isFirstUsage
+            context.getDrawable(R.drawable.ic_password)!!
           )
         }
         is CreatePublicKeyCredentialRequest -> {
@@ -275,8 +275,7 @@
             displayName,
             createCredentialRequestJetpack.type,
             requestInfo.appPackageName,
-            context.getDrawable(R.drawable.ic_passkey)!!,
-            requestInfo.isFirstUsage)
+            context.getDrawable(R.drawable.ic_passkey)!!)
         }
         // TODO: correctly parsing for other sign-ins
         else -> {
@@ -285,20 +284,60 @@
             "Elisa Beckett",
             "other-sign-ins",
             requestInfo.appPackageName,
-            context.getDrawable(R.drawable.ic_other_sign_in)!!,
-            requestInfo.isFirstUsage)
+            context.getDrawable(R.drawable.ic_other_sign_in)!!)
         }
       }
     }
 
-    fun toCreateScreenState(
+    fun toCreateCredentialUiState(
+      enabledProviders: List<EnabledProviderInfo>,
+      disabledProviders: List<DisabledProviderInfo>?,
+      requestDisplayInfo: RequestDisplayInfo,
+      isOnPasskeyIntroStateAlready: Boolean,
+    ): CreateCredentialUiState {
+      var createOptionSize = 0
+      var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
+      var remoteEntry: RemoteInfo? = null
+      var defaultProvider: EnabledProviderInfo? = null
+      val defaultProviderId = UserConfigRepo.getInstance().getDefaultProviderId()
+      enabledProviders.forEach {
+          enabledProvider ->
+        if (defaultProviderId != null) {
+          if (enabledProvider.name == defaultProviderId) {
+            defaultProvider = enabledProvider
+          }
+        }
+        if (enabledProvider.createOptions.isNotEmpty()) {
+          createOptionSize += enabledProvider.createOptions.size
+          lastSeenProviderWithNonEmptyCreateOptions = enabledProvider
+        }
+        if (enabledProvider.remoteEntry != null) {
+          remoteEntry = enabledProvider.remoteEntry!!
+        }
+      }
+      return CreateCredentialUiState(
+        enabledProviders = enabledProviders,
+        disabledProviders = disabledProviders,
+        toCreateScreenState(
+          createOptionSize, isOnPasskeyIntroStateAlready,
+          requestDisplayInfo, defaultProvider, remoteEntry),
+        requestDisplayInfo,
+        isOnPasskeyIntroStateAlready,
+        toActiveEntry(
+          /*defaultProvider=*/defaultProvider, createOptionSize,
+          lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
+      )
+    }
+
+    private fun toCreateScreenState(
       createOptionSize: Int,
       isOnPasskeyIntroStateAlready: Boolean,
       requestDisplayInfo: RequestDisplayInfo,
       defaultProvider: EnabledProviderInfo?,
       remoteEntry: RemoteInfo?,
     ): CreateScreenState {
-      return if (requestDisplayInfo.isFirstUsage && requestDisplayInfo
+      return if (
+        UserConfigRepo.getInstance().getIsFirstUse() && requestDisplayInfo
           .type == TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) {
         CreateScreenState.PASSKEY_INTRO
       } else if (
@@ -313,12 +352,12 @@
       } 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.")
+        // TODO: properly handle error and gracefully finish itself
+        throw java.lang.IllegalStateException("Empty provider list.")
       }
     }
 
-   fun toActiveEntry(
+    private fun toActiveEntry(
       defaultProvider: EnabledProviderInfo?,
       createOptionSize: Int,
       lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo?,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt
new file mode 100644
index 0000000..5e77663
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.credentialmanager
+
+import android.content.Context
+import android.content.SharedPreferences
+
+class UserConfigRepo(context: Context) {
+    val sharedPreferences: SharedPreferences = context.getSharedPreferences(
+        context.packageName, Context.MODE_PRIVATE)
+
+    fun setDefaultProvider(
+        providerId: String
+    ) {
+        sharedPreferences.edit().apply {
+            putString(DEFAULT_PROVIDER, providerId)
+            apply()
+        }
+    }
+
+    fun setIsFirstUse(
+        isFirstUse: Boolean
+    ) {
+        sharedPreferences.edit().apply {
+            putBoolean(IS_PASSKEY_FIRST_USE, isFirstUse)
+            apply()
+        }
+    }
+
+    fun getDefaultProviderId(): String? {
+        return sharedPreferences.getString(DEFAULT_PROVIDER, null)
+    }
+
+    fun getIsFirstUse(): Boolean {
+        return sharedPreferences.getBoolean(IS_PASSKEY_FIRST_USE, true)
+    }
+
+    companion object {
+        lateinit var repo: UserConfigRepo
+
+        const val DEFAULT_PROVIDER = "default_provider"
+        // This first use value only applies to passkeys, not related with if generally
+        // credential manager is first use or not
+        const val IS_PASSKEY_FIRST_USE = "is_passkey_first_use"
+
+        fun setup(
+            context: Context,
+        ) {
+            repo = UserConfigRepo(context)
+        }
+
+        fun getInstance(): UserConfigRepo {
+            return repo
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index c26fac8..38e2caa 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -108,7 +108,8 @@
                     )
                     CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
                         providerInfo = uiState.activeEntry?.activeProvider!!,
-                        onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected
+                        onChangeDefaultSelected = viewModel::onChangeDefaultSelected,
+                        onUseOnceSelected = viewModel::onUseOnceSelected,
                     )
                     CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard(
                         requestDisplayInfo = uiState.requestDisplayInfo,
@@ -464,7 +465,8 @@
 @Composable
 fun MoreOptionsRowIntroCard(
     providerInfo: EnabledProviderInfo,
-    onDefaultOrNotSelected: () -> Unit,
+    onChangeDefaultSelected: () -> Unit,
+    onUseOnceSelected: () -> Unit,
 ) {
     ContainerCard() {
         Column() {
@@ -496,11 +498,11 @@
             ) {
                 CancelButton(
                     stringResource(R.string.use_once),
-                    onClick = onDefaultOrNotSelected
+                    onClick = onUseOnceSelected
                 )
                 ConfirmButton(
                     stringResource(R.string.set_as_default),
-                    onClick = onDefaultOrNotSelected
+                    onClick = onChangeDefaultSelected
                 )
             }
             Divider(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
index 518aaee..9d029dff 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
@@ -29,6 +29,7 @@
 import androidx.lifecycle.ViewModel
 import com.android.credentialmanager.CreateFlowUtils
 import com.android.credentialmanager.CredentialManagerRepo
+import com.android.credentialmanager.UserConfigRepo
 import com.android.credentialmanager.common.DialogResult
 import com.android.credentialmanager.common.ProviderActivityResult
 import com.android.credentialmanager.common.ResultState
@@ -61,32 +62,15 @@
   }
 
   fun onConfirmIntro() {
-    var createOptionSize = 0
-    var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
-    var remoteEntry: RemoteInfo? = null
-    uiState.enabledProviders.forEach {
-      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),
-    )
+    uiState = CreateFlowUtils.toCreateCredentialUiState(
+      uiState.enabledProviders, uiState.disabledProviders,
+      uiState.requestDisplayInfo, true)
+    UserConfigRepo.getInstance().setIsFirstUse(false)
   }
 
   fun getProviderInfoByName(providerName: String): EnabledProviderInfo {
     return uiState.enabledProviders.single {
-      it.name.equals(providerName)
+      it.name == providerName
     }
   }
 
@@ -116,6 +100,8 @@
       showActiveEntryOnly = true,
       activeEntry = activeEntry
     )
+    val providerId = uiState.activeEntry?.activeProvider?.name
+    onDefaultChanged(providerId)
   }
 
   fun onDisabledPasswordManagerSelected() {
@@ -127,11 +113,29 @@
     dialogResult.value = DialogResult(ResultState.CANCELED)
   }
 
-  fun onDefaultOrNotSelected() {
+  fun onChangeDefaultSelected() {
     uiState = uiState.copy(
       currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
     )
-    // TODO: implement the if choose as default or not logic later
+    val providerId = uiState.activeEntry?.activeProvider?.name
+    onDefaultChanged(providerId)
+  }
+
+  fun onUseOnceSelected() {
+    uiState = uiState.copy(
+      currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+    )
+  }
+
+  fun onDefaultChanged(providerId: String?) {
+    if (providerId != null) {
+      Log.d(
+        "Account Selector", "Default provider changed to: " +
+                " {provider=$providerId")
+      UserConfigRepo.getInstance().setDefaultProvider(providerId)
+    } else {
+      Log.w("Account Selector", "Null provider is being changed")
+    }
   }
 
   fun onEntrySelected(selectedEntry: EntryInfo) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 21abe08..fda0b97 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -31,7 +31,6 @@
   name: String,
   displayName: String,
   var createOptions: List<CreateOptionInfo>,
-  val isDefault: Boolean,
   var remoteEntry: RemoteInfo?,
 ) : ProviderInfo(icon, name, displayName)
 
@@ -77,7 +76,6 @@
   val type: String,
   val appDomainName: String,
   val typeIcon: Drawable,
-  val isFirstUsage: Boolean,
 )
 
 /**
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index 6a3b239..b713c14 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -119,6 +119,10 @@
     <string name="uninstall_update_text_multiuser">Replace this app with the factory version? All data will be removed. This affects all users of this device, including those with work profiles.</string>
     <!-- Label of a checkbox that allows to keep the data (e.g. files, settings) of the app on uninstall [CHAR LIMIT=none] -->
     <string name="uninstall_keep_data">Keep <xliff:g id="size" example="1.5MB">%1$s</xliff:g> of app data.</string>
+    <!--  [CHAR LIMIT=none] -->
+    <string name="uninstall_application_text_current_user_clone_profile">Do you want to delete this app?</string>
+    <!--  [CHAR LIMIT=none] -->
+    <string name="uninstall_application_text_with_clone_instance">Do you want to uninstall this app? <xliff:g id="package_label">%1$s</xliff:g> clone will also be deleted.</string>
 
     <!-- Label for the notification channel containing notifications for current uninstall operations [CHAR LIMIT=40] -->
     <string name="uninstalling_notification_channel">Running uninstalls</string>
@@ -137,6 +141,8 @@
     <string name="uninstall_failed">Uninstall unsuccessful.</string>
     <!-- [CHAR LIMIT=100] -->
     <string name="uninstall_failed_app">Uninstalling <xliff:g id="package_label">%1$s</xliff:g> unsuccessful.</string>
+    <!-- [CHAR LIMIT=100] -->
+    <string name="uninstalling_cloned_app">Deleting <xliff:g id="package_label">%1$s</xliff:g> clone\u2026</string>
     <!-- String presented to the user when uninstalling a package failed because the target package
         is a current device administrator [CHAR LIMIT=80] -->
     <string name="uninstall_failed_device_policy_manager">Can\'t uninstall active device admin
@@ -219,6 +225,9 @@
         TV or loss of data that may result from its use.
     </string>
 
+    <!-- Label for cloned app in uninstall dialogue [CHAR LIMIT=40] -->
+    <string name="cloned_app_label"><xliff:g id="package_label">%1$s</xliff:g> Clone</string>
+
     <!-- Label for button to continue install of an app whose source cannot be identified [CHAR LIMIT=40] -->
     <string name="anonymous_source_continue">Continue</string>
 
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
index 7bf27df..1485352 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
@@ -33,8 +33,10 @@
 import android.content.pm.VersionedPackage;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Log;
 import android.widget.Toast;
 
@@ -51,6 +53,7 @@
 
     static final String EXTRA_APP_LABEL = "com.android.packageinstaller.extra.APP_LABEL";
     static final String EXTRA_KEEP_DATA = "com.android.packageinstaller.extra.KEEP_DATA";
+    public static final String EXTRA_IS_CLONE_USER = "isCloneUser";
 
     private int mUninstallId;
     private ApplicationInfo mAppInfo;
@@ -76,6 +79,18 @@
                 boolean keepData = getIntent().getBooleanExtra(EXTRA_KEEP_DATA, false);
                 UserHandle user = getIntent().getParcelableExtra(Intent.EXTRA_USER);
 
+                boolean isCloneUser = false;
+                if (user == null) {
+                    user = Process.myUserHandle();
+                }
+
+                UserManager customUserManager = UninstallUninstalling.this
+                        .createContextAsUser(UserHandle.of(user.getIdentifier()), 0)
+                        .getSystemService(UserManager.class);
+                if (customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE)) {
+                    isCloneUser = true;
+                }
+
                 // Show dialog, which is the whole UI
                 FragmentTransaction transaction = getFragmentManager().beginTransaction();
                 Fragment prev = getFragmentManager().findFragmentByTag("dialog");
@@ -83,6 +98,9 @@
                     transaction.remove(prev);
                 }
                 DialogFragment dialog = new UninstallUninstallingFragment();
+                Bundle args = new Bundle();
+                args.putBoolean(EXTRA_IS_CLONE_USER, isCloneUser);
+                dialog.setArguments(args);
                 dialog.setCancelable(false);
                 dialog.show(transaction, "dialog");
 
@@ -176,9 +194,20 @@
         public Dialog onCreateDialog(Bundle savedInstanceState) {
             AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
 
+            Bundle bundle = getArguments();
+            boolean isCloneUser = false;
+            if (bundle != null) {
+                isCloneUser = bundle.getBoolean(EXTRA_IS_CLONE_USER);
+            }
+
             dialogBuilder.setCancelable(false);
-            dialogBuilder.setMessage(getActivity().getString(R.string.uninstalling_app,
-                    ((UninstallUninstalling) getActivity()).mLabel));
+            if (isCloneUser) {
+                dialogBuilder.setMessage(getActivity().getString(R.string.uninstalling_cloned_app,
+                        ((UninstallUninstalling) getActivity()).mLabel));
+            } else {
+                dialogBuilder.setMessage(getActivity().getString(R.string.uninstalling_app,
+                        ((UninstallUninstalling) getActivity()).mLabel));
+            }
 
             Dialog dialog = dialogBuilder.create();
             dialog.setCanceledOnTouchOutside(false);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
index c9230b4..a1bc992 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
@@ -123,6 +123,7 @@
                 messageBuilder.append(" ").append(appLabel).append(".\n\n");
             }
         }
+        boolean isClonedApp = false;
 
         final boolean isUpdate =
                 ((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
@@ -144,16 +145,36 @@
                     messageBuilder.append(
                             getString(R.string.uninstall_application_text_current_user_work_profile,
                                     userInfo.name));
+                } else if (userInfo.isCloneProfile()
+                        && userInfo.profileGroupId == myUserHandle.getIdentifier()) {
+                    isClonedApp = true;
+                    messageBuilder.append(getString(
+                            R.string.uninstall_application_text_current_user_clone_profile));
                 } else {
                     messageBuilder.append(
                             getString(R.string.uninstall_application_text_user, userInfo.name));
                 }
+            } else if (isCloneProfile(myUserHandle)) {
+                isClonedApp = true;
+                messageBuilder.append(getString(
+                        R.string.uninstall_application_text_current_user_clone_profile));
             } else {
-                messageBuilder.append(getString(R.string.uninstall_application_text));
+                if (Process.myUserHandle().equals(UserHandle.SYSTEM)
+                        && hasClonedInstance(dialogInfo.appInfo.packageName)) {
+                    messageBuilder.append(getString(
+                            R.string.uninstall_application_text_with_clone_instance,
+                            appLabel));
+                } else {
+                    messageBuilder.append(getString(R.string.uninstall_application_text));
+                }
             }
         }
 
-        dialogBuilder.setTitle(appLabel);
+        if (isClonedApp) {
+            dialogBuilder.setTitle(getString(R.string.cloned_app_label, appLabel));
+        } else {
+            dialogBuilder.setTitle(appLabel);
+        }
         dialogBuilder.setPositiveButton(android.R.string.ok, this);
         dialogBuilder.setNegativeButton(android.R.string.cancel, this);
 
@@ -192,6 +213,42 @@
         return dialogBuilder.create();
     }
 
+    private boolean isCloneProfile(UserHandle userHandle) {
+        UserManager customUserManager = getContext()
+                .createContextAsUser(UserHandle.of(userHandle.getIdentifier()), 0)
+                .getSystemService(UserManager.class);
+        if (customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE)) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean hasClonedInstance(String packageName) {
+        // Check if clone user is present on the device.
+        UserHandle cloneUser = null;
+        UserManager userManager = getContext().getSystemService(UserManager.class);
+        List<UserHandle> profiles = userManager.getUserProfiles();
+        for (UserHandle userHandle : profiles) {
+            if (!Process.myUserHandle().equals(UserHandle.SYSTEM) && isCloneProfile(userHandle)) {
+                cloneUser = userHandle;
+                break;
+            }
+        }
+
+        // Check if another instance of given package exists in clone user profile.
+        if (cloneUser != null) {
+            try {
+                if (getContext().getPackageManager()
+                        .getPackageUidAsUser(packageName, cloneUser.getIdentifier()) > 0) {
+                    return true;
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
     @Override
     public void onClick(DialogInterface dialog, int which) {
         if (which == Dialog.BUTTON_POSITIVE) {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
index 5ae5ada..62db7bd 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
@@ -93,7 +93,7 @@
         ) {
             val context = LocalContext.current
             val internalListModel = remember {
-                TogglePermissionInternalAppListModel(context, listModel)
+                TogglePermissionInternalAppListModel(context, listModel, ::RestrictionsProviderImpl)
             }
             val record = remember { listModel.transformItem(app) }
             if (!remember { listModel.isChangeable(record) }) return
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
index 00eb607..f65e310 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
@@ -42,6 +42,7 @@
 import com.android.settingslib.spaprivileged.model.app.AppRecord
 import com.android.settingslib.spaprivileged.model.app.userId
 import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
 import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
 import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference
 import kotlinx.coroutines.flow.Flow
@@ -64,41 +65,18 @@
         val permissionType = parameter.getStringArg(PERMISSION, arguments)!!
         val appListPage = SettingsPage.create(name, parameter = parameter, arguments = arguments)
         val appInfoPage = TogglePermissionAppInfoPageProvider.buildPageData(permissionType)
-        val entryList = mutableListOf<SettingsEntry>()
         // TODO: add more categories, such as personal, work, cloned, etc.
-        for (category in listOf("personal")) {
-            entryList.add(
-                SettingsEntryBuilder.createLinkFrom("${ENTRY_NAME}_$category", appListPage)
-                    .setLink(toPage = appInfoPage)
-                    .build()
-            )
+        return listOf("personal").map { category ->
+            SettingsEntryBuilder.createLinkFrom("${ENTRY_NAME}_$category", appListPage)
+                .setLink(toPage = appInfoPage)
+                .build()
         }
-        return entryList
     }
 
     @Composable
     override fun Page(arguments: Bundle?) {
-        TogglePermissionAppList(arguments?.getString(PERMISSION)!!)
-    }
-
-    @Composable
-    private fun TogglePermissionAppList(permissionType: String) {
-        val listModel = appListTemplate.rememberModel(permissionType)
-        val context = LocalContext.current
-        val internalListModel = remember {
-            TogglePermissionInternalAppListModel(context, listModel)
-        }
-        AppListPage(
-            title = stringResource(listModel.pageTitleResId),
-            listModel = internalListModel,
-        ) {
-            AppListItem(
-                onClick = TogglePermissionAppInfoPageProvider.navigator(
-                    permissionType = permissionType,
-                    app = record.app,
-                ),
-            )
-        }
+        val permissionType = arguments?.getString(PERMISSION)!!
+        appListTemplate.rememberModel(permissionType).TogglePermissionAppList(permissionType)
     }
 
     companion object {
@@ -132,9 +110,34 @@
     }
 }
 
+@Composable
+internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppList(
+    permissionType: String,
+    restrictionsProviderFactory: RestrictionsProviderFactory = ::RestrictionsProviderImpl,
+    appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
+) {
+    val context = LocalContext.current
+    val internalListModel = remember {
+        TogglePermissionInternalAppListModel(context, this, restrictionsProviderFactory)
+    }
+    AppListPage(
+        title = stringResource(pageTitleResId),
+        listModel = internalListModel,
+        appList = appList,
+    ) {
+        AppListItem(
+            onClick = TogglePermissionAppInfoPageProvider.navigator(
+                permissionType = permissionType,
+                app = record.app,
+            ),
+        )
+    }
+}
+
 internal class TogglePermissionInternalAppListModel<T : AppRecord>(
     private val context: Context,
     private val listModel: TogglePermissionAppListModel<T>,
+    private val restrictionsProviderFactory: RestrictionsProviderFactory,
 ) : AppListModel<T> {
     override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
         listModel.transform(userIdFlow, appListFlow)
@@ -147,12 +150,12 @@
 
     @Composable
     fun getSummary(record: T): State<String> {
-        val restrictionsProvider = remember {
+        val restrictionsProvider = remember(record.app.userId) {
             val restrictions = Restrictions(
                 userId = record.app.userId,
                 keys = listModel.switchRestrictionKeys,
             )
-            RestrictionsProviderImpl(context, restrictions)
+            restrictionsProviderFactory(context, restrictions)
         }
         val restrictedMode = restrictionsProvider.restrictedModeState()
         val allowed = listModel.isAllowed(record)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt
index 8c1421a..86b6f02 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.spaprivileged.template.scaffold
 
-import android.content.Context
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
@@ -24,7 +23,7 @@
 import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
 import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
 import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
-import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
 import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
 
 @Composable
@@ -41,7 +40,7 @@
     text: String,
     restrictions: Restrictions,
     onClick: () -> Unit,
-    restrictionsProviderFactory: (Context, Restrictions) -> RestrictionsProvider,
+    restrictionsProviderFactory: RestrictionsProviderFactory,
 ) {
     val context = LocalContext.current
     val restrictionsProvider = remember(restrictions) {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
index 355dfb6..75b884c 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
@@ -17,14 +17,22 @@
 package com.android.settingslib.spaprivileged.template.app
 
 import android.content.Context
+import android.content.pm.ApplicationInfo
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.State
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spaprivileged.test.R
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.testutils.FakeNavControllerWrapper
+import com.android.settingslib.spaprivileged.R
+import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
+import com.android.settingslib.spaprivileged.tests.testutils.FakeRestrictionsProvider
+import com.android.settingslib.spaprivileged.tests.testutils.TestAppRecord
 import com.android.settingslib.spaprivileged.tests.testutils.TestTogglePermissionAppListModel
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -38,10 +46,97 @@
 
     private val context: Context = ApplicationProvider.getApplicationContext()
 
+    private val fakeNavControllerWrapper = FakeNavControllerWrapper()
+
+    private val fakeRestrictionsProvider = FakeRestrictionsProvider()
+
     @Test
-    fun appListInjectEntry_titleDisplayed() {
+    fun internalAppListModel_whenAllowed() {
+        fakeRestrictionsProvider.restrictedMode = NoRestricted
+        val listModel = TestTogglePermissionAppListModel(isAllowed = true)
+        val internalAppListModel = TogglePermissionInternalAppListModel(
+            context = context,
+            listModel = listModel,
+            restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+        )
+
+        val summaryState = getSummary(internalAppListModel)
+
+        assertThat(summaryState.value).isEqualTo(
+            context.getString(R.string.app_permission_summary_allowed)
+        )
+    }
+
+    @Test
+    fun internalAppListModel_whenNotAllowed() {
+        fakeRestrictionsProvider.restrictedMode = NoRestricted
+        val listModel = TestTogglePermissionAppListModel(isAllowed = false)
+        val internalAppListModel = TogglePermissionInternalAppListModel(
+            context = context,
+            listModel = listModel,
+            restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+        )
+
+        val summaryState = getSummary(internalAppListModel)
+
+        assertThat(summaryState.value).isEqualTo(
+            context.getString(R.string.app_permission_summary_not_allowed)
+        )
+    }
+
+    @Test
+    fun internalAppListModel_whenComputingAllowed() {
+        fakeRestrictionsProvider.restrictedMode = NoRestricted
+        val listModel = TestTogglePermissionAppListModel(isAllowed = null)
+        val internalAppListModel = TogglePermissionInternalAppListModel(
+            context = context,
+            listModel = listModel,
+            restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+        )
+
+        val summaryState = getSummary(internalAppListModel)
+
+        assertThat(summaryState.value).isEqualTo(
+            context.getString(R.string.summary_placeholder)
+        )
+    }
+
+    @Test
+    fun appListItem_onClick_navigate() {
+        val listModel = TestTogglePermissionAppListModel()
+        composeTestRule.setContent {
+            listModel.TogglePermissionAppList(
+                permissionType = PERMISSION_TYPE,
+                restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+            ) {
+                fakeNavControllerWrapper.Wrapper {
+                    AppListItemModel(
+                        record = listModel.transformItem(APP),
+                        label = LABEL,
+                        summary = stateOf(SUMMARY),
+                    ).appItem()
+                }
+            }
+        }
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+
+        assertThat(fakeNavControllerWrapper.navigateCalledWith)
+            .isEqualTo("TogglePermissionAppInfoPage/test.PERMISSION/package.name/0")
+    }
+
+    @Test
+    fun getRoute() {
+        val route = TogglePermissionAppListPageProvider.getRoute(PERMISSION_TYPE)
+
+        assertThat(route).isEqualTo("TogglePermissionAppList/test.PERMISSION")
+    }
+
+    @Test
+    fun buildInjectEntry_titleDisplayed() {
+        val listModel = TestTogglePermissionAppListModel()
         val entry = TogglePermissionAppListPageProvider.buildInjectEntry(PERMISSION_TYPE) {
-            TestTogglePermissionAppListModel()
+            listModel
         }.build()
 
         composeTestRule.setContent {
@@ -50,18 +145,27 @@
             }
         }
 
-        composeTestRule.onNodeWithText(context.getString(R.string.test_permission_title))
+        composeTestRule.onNodeWithText(context.getString(listModel.pageTitleResId))
             .assertIsDisplayed()
     }
 
-    @Test
-    fun appListRoute() {
-        val route = TogglePermissionAppListPageProvider.getRoute(PERMISSION_TYPE)
-
-        assertThat(route).isEqualTo("TogglePermissionAppList/test.PERMISSION")
+    private fun getSummary(
+        internalAppListModel: TogglePermissionInternalAppListModel<TestAppRecord>,
+    ): State<String> {
+        lateinit var summary: State<String>
+        composeTestRule.setContent {
+            summary = internalAppListModel.getSummary(record = TestAppRecord(APP))
+        }
+        return summary
     }
 
     private companion object {
         const val PERMISSION_TYPE = "test.PERMISSION"
+        const val PACKAGE_NAME = "package.name"
+        const val LABEL = "Label"
+        const val SUMMARY = "Summary"
+        val APP = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+        }
     }
 }
diff --git a/packages/SettingsLib/res/values-af/strings.xml b/packages/SettingsLib/res/values-af/strings.xml
index 0234330..fb46dfb 100644
--- a/packages/SettingsLib/res/values-af/strings.xml
+++ b/packages/SettingsLib/res/values-af/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> oor tot vol"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> oor tot vol"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laaiproses is onderbreek"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Laaiproses is onderbreek"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laai tot <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Onbekend"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Laai"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Laai tans vinnig"</string>
diff --git a/packages/SettingsLib/res/values-am/strings.xml b/packages/SettingsLib/res/values-am/strings.xml
index 29b188c..fa53f0d 100644
--- a/packages/SettingsLib/res/values-am/strings.xml
+++ b/packages/SettingsLib/res/values-am/strings.xml
@@ -233,7 +233,7 @@
     <string name="adb_pair_method_qrcode_title" msgid="6982904096137468634">"የQR ኮድን በመጠቀም መሣሪያን ያጣምሩ"</string>
     <string name="adb_pair_method_qrcode_summary" msgid="7130694277228970888">"የQR ኮድ መቃኛን በመጠቀም አዲስ መሣሪያዎችን ያጣምሩ"</string>
     <string name="adb_pair_method_code_title" msgid="1122590300445142904">"የማጣመሪያ ኮድን በመጠቀም መሣሪያን ያጣምሩ"</string>
-    <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"የስድስት አኃዝ ኮድ በመጠቀም አዲስ መሣሪያዎችን ያጣምሩ"</string>
+    <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"የስድስት አሃዝ ኮድ በመጠቀም አዲስ መሣሪያዎችን ያጣምሩ"</string>
     <string name="adb_paired_devices_title" msgid="5268997341526217362">"የተጣመሩ መሣሪያዎች"</string>
     <string name="adb_wireless_device_connected_summary" msgid="3039660790249148713">"አሁን ላይ ተገናኝቷል"</string>
     <string name="adb_wireless_device_details_title" msgid="7129369670526565786">"የመሣሪያ ዝርዝሮች"</string>
@@ -344,7 +344,7 @@
     <string name="wait_for_debugger" msgid="7461199843335409809">"ስህተት ማስወገጃውን ጠብቅ"</string>
     <string name="wait_for_debugger_summary" msgid="6846330006113363286">"ስህተቱ የተወገደለት መተግበሪያ ከመፈጸሙ በፊት የስህተት ማስወገጃው እስኪያያዝ ድረስ እየጠበቀው ነው"</string>
     <string name="debug_input_category" msgid="7349460906970849771">"ግብዓት"</string>
-    <string name="debug_drawing_category" msgid="5066171112313666619">"ስዕል"</string>
+    <string name="debug_drawing_category" msgid="5066171112313666619">"ሥዕል"</string>
     <string name="debug_hw_drawing_category" msgid="5830815169336975162">"የተፋጠነ የሃርድዌር አሰጣጥ"</string>
     <string name="media_category" msgid="8122076702526144053">"ማህደረመረጃ"</string>
     <string name="debug_monitoring_category" msgid="1597387133765424994">"ቁጥጥር"</string>
@@ -467,7 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"እስኪሞላ ድረስ <xliff:g id="TIME">%1$s</xliff:g> ይቀራል"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - እስኪሞላ ድረስ <xliff:g id="TIME">%2$s</xliff:g> ይቀራል"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ኃይል መሙላት ባለበት ቆሟል"</string>
+    <!-- no translation found for power_charging_limited (6732738149313642521) -->
+    <skip />
     <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
     <skip />
     <string name="battery_info_status_unknown" msgid="268625384868401114">"ያልታወቀ"</string>
@@ -548,10 +549,10 @@
     <string name="shared_data_title" msgid="1017034836800864953">"የተጋራ ውሂብ"</string>
     <string name="shared_data_summary" msgid="5516326713822885652">"የተጋራ ውሂብን ይመልከቱ እና ያሻሽሉ"</string>
     <string name="shared_data_no_blobs_text" msgid="3108114670341737434">"ለዚህ ተጠቃሚ ምንም የተጋራ ውሂብ የለም።"</string>
-    <string name="shared_data_query_failure_text" msgid="3489828881998773687">"የተጋራውን ውሂብ በማግኘት ላይ ስሕተት ነበረ። እንደገና ይሞክሩ።"</string>
+    <string name="shared_data_query_failure_text" msgid="3489828881998773687">"የተጋራውን ውሂብ በማግኘት ላይ ስህተት ነበረ። እንደገና ይሞክሩ።"</string>
     <string name="blob_id_text" msgid="8680078988996308061">"የተጋራ ውሂብ መታወቂያ፦ <xliff:g id="BLOB_ID">%d</xliff:g>"</string>
     <string name="blob_expires_text" msgid="7882727111491739331">"በ<xliff:g id="DATE">%s</xliff:g> ላይ የአገልግሎት ጊዜው ያበቃል"</string>
-    <string name="shared_data_delete_failure_text" msgid="3842701391009628947">"የተጋራውን ውሂብ በመሰረዝ ላይ ስሕተት ነበረ።"</string>
+    <string name="shared_data_delete_failure_text" msgid="3842701391009628947">"የተጋራውን ውሂብ በመሰረዝ ላይ ስህተት ነበረ።"</string>
     <string name="shared_data_no_accessors_dialog_text" msgid="8903738462570715315">"ለዚህ የተጋራ ውሂብ ምንም የሚያስፈልጉ ኪራዮች የሉም። ሊሰርዙት ይፈልጋሉ?"</string>
     <string name="accessor_info_title" msgid="8289823651512477787">"ውሂብ የሚጋሩ መተግበሪያዎች"</string>
     <string name="accessor_no_description_text" msgid="7510967452505591456">"በመተግበሪያው ምንም ዝርዝር መረጃ አልተሰጠም።"</string>
diff --git a/packages/SettingsLib/res/values-ar/strings.xml b/packages/SettingsLib/res/values-ar/strings.xml
index 28a3f18..88db7be 100644
--- a/packages/SettingsLib/res/values-ar/strings.xml
+++ b/packages/SettingsLib/res/values-ar/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"يتبقّى <xliff:g id="TIME">%1$s</xliff:g> حتى اكتمال شحن البطارية."</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - يتبقّى <xliff:g id="TIME">%2$s</xliff:g> حتى اكتمال شحن البطارية."</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - تم إيقاف الشحن مؤقتًا"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - الشحن متوقّف مؤقتًا"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - الشحن حتى <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"غير معروف"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"جارٍ الشحن"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"جارٍ الشحن سريعًا"</string>
diff --git a/packages/SettingsLib/res/values-as/strings.xml b/packages/SettingsLib/res/values-as/strings.xml
index fc54e04..3f068b4 100644
--- a/packages/SettingsLib/res/values-as/strings.xml
+++ b/packages/SettingsLib/res/values-as/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"সম্পূৰ্ণ হ’বলৈ <xliff:g id="TIME">%1$s</xliff:g> বাকী আছে"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"সম্পূৰ্ণ হ’বলৈ <xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> বাকী আছে"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - চাৰ্জিং পজ কৰা হৈছে"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - চাৰ্জিং পজ কৰা হৈছে"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>লৈ চাৰ্জিং"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"অজ্ঞাত"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"চাৰ্জ কৰি থকা হৈছে"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"দ্ৰুততাৰে চাৰ্জ হৈছে"</string>
diff --git a/packages/SettingsLib/res/values-az/strings.xml b/packages/SettingsLib/res/values-az/strings.xml
index e2e294f..8eb8d5b 100644
--- a/packages/SettingsLib/res/values-az/strings.xml
+++ b/packages/SettingsLib/res/values-az/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Tam şarj edilənədək <xliff:g id="TIME">%1$s</xliff:g> qalıb"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - tam şarj edilənədək <xliff:g id="TIME">%2$s</xliff:g> qalıb"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Şarj durdurulub"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Şarj durdurulub"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> olana qədər şarj edilir"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Naməlum"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Enerji doldurma"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Sürətlə doldurulur"</string>
diff --git a/packages/SettingsLib/res/values-b+sr+Latn/strings.xml b/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
index 479c9ca..bbdd8bf 100644
--- a/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
+++ b/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do kraja punjenja"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do kraja punjenja"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Punjenje je zaustavljeno"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Punjenje je pauzirano"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – punjenje do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Nepoznato"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Puni se"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Brzo se puni"</string>
diff --git a/packages/SettingsLib/res/values-be/strings.xml b/packages/SettingsLib/res/values-be/strings.xml
index b45996c..28c1e86 100644
--- a/packages/SettingsLib/res/values-be/strings.xml
+++ b/packages/SettingsLib/res/values-be/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Да поўнай зарадкі засталося <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – да поўнай зарадкі засталося: <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Зарадка прыпынена"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Зарадка прыпынена"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – зарадка да <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Невядома"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Зарадка"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Хуткая зарадка"</string>
diff --git a/packages/SettingsLib/res/values-bg/strings.xml b/packages/SettingsLib/res/values-bg/strings.xml
index ecb5208..61e441d 100644
--- a/packages/SettingsLib/res/values-bg/strings.xml
+++ b/packages/SettingsLib/res/values-bg/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Оставащо време до пълно зареждане: <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – Оставащо време до пълно зареждане: <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: Зареждането е на пауза"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – зареждането е на пауза"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – зарежда се до <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Неизвестно"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Зарежда се"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Зарежда се бързо"</string>
diff --git a/packages/SettingsLib/res/values-bn/strings.xml b/packages/SettingsLib/res/values-bn/strings.xml
index fda27ed..a9d32a9 100644
--- a/packages/SettingsLib/res/values-bn/strings.xml
+++ b/packages/SettingsLib/res/values-bn/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g>-এ ব্যাটারি পুরো চার্জ হয়ে যাবে"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g>-এ ব্যাটারি পুরো চার্জ হয়ে যাবে"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - চার্জিং পজ করা হয়েছে"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - চার্জিং পজ করা হয়েছে"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> পর্যন্ত চার্জ হচ্ছে"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"অজানা"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"চার্জ হচ্ছে"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"দ্রুত চার্জ হচ্ছে"</string>
diff --git a/packages/SettingsLib/res/values-bs/strings.xml b/packages/SettingsLib/res/values-bs/strings.xml
index cae96e9..b5c9177 100644
--- a/packages/SettingsLib/res/values-bs/strings.xml
+++ b/packages/SettingsLib/res/values-bs/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do potpune napunjenosti"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do potpune napunjenosti"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Punjenje je pauzirano"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Punjenje je pauzirano"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Punjenje do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Nepoznato"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Punjenje"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Brzo punjenje"</string>
diff --git a/packages/SettingsLib/res/values-ca/strings.xml b/packages/SettingsLib/res/values-ca/strings.xml
index 51a4a02..8add8c8 100644
--- a/packages/SettingsLib/res/values-ca/strings.xml
+++ b/packages/SettingsLib/res/values-ca/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> per completar la càrrega"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> per completar la càrrega"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: la càrrega s\'ha posat en pausa"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g>: la càrrega s\'ha posat en pausa"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>: s\'està carregant fins al <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Desconegut"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"S\'està carregant"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carregant ràpidament"</string>
diff --git a/packages/SettingsLib/res/values-cs/strings.xml b/packages/SettingsLib/res/values-cs/strings.xml
index a50e527..4dc3c66 100644
--- a/packages/SettingsLib/res/values-cs/strings.xml
+++ b/packages/SettingsLib/res/values-cs/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do úplného nabití"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do úplného nabití"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Nabíjení je pozastaveno"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Nabíjení pozastaveno"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – nabíjení do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Neznámé"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Nabíjí se"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Rychlé nabíjení"</string>
diff --git a/packages/SettingsLib/res/values-da/strings.xml b/packages/SettingsLib/res/values-da/strings.xml
index 95e0130..9ab499a 100644
--- a/packages/SettingsLib/res/values-da/strings.xml
+++ b/packages/SettingsLib/res/values-da/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Fuldt opladet om <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – fuldt opladet om <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Opladningen er sat på pause"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Opladning er sat på pause"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Oplader til <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Ukendt"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Oplader"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Oplader hurtigt"</string>
diff --git a/packages/SettingsLib/res/values-de/strings.xml b/packages/SettingsLib/res/values-de/strings.xml
index 12fce37..68d61c0 100644
--- a/packages/SettingsLib/res/values-de/strings.xml
+++ b/packages/SettingsLib/res/values-de/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Voll in <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – voll in <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Ladevorgang angehalten"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laden pausiert"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Aufladung auf <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Unbekannt"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Wird aufgeladen"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Schnelles Aufladen"</string>
diff --git a/packages/SettingsLib/res/values-el/strings.xml b/packages/SettingsLib/res/values-el/strings.xml
index f54d5d2..59ae638 100644
--- a/packages/SettingsLib/res/values-el/strings.xml
+++ b/packages/SettingsLib/res/values-el/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Απομένουν <xliff:g id="TIME">%1$s</xliff:g> για πλήρη φόρτιση"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - Απομένουν <xliff:g id="TIME">%2$s</xliff:g> για πλήρη φόρτιση"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Η φόρτιση τέθηκε σε παύση"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Φόρτιση σε παύση"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Φόρτιση έως το <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Άγνωστο"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Φόρτιση"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Ταχεία φόρτιση"</string>
diff --git a/packages/SettingsLib/res/values-en-rAU/strings.xml b/packages/SettingsLib/res/values-en-rAU/strings.xml
index 208dbfa..3c98c25 100644
--- a/packages/SettingsLib/res/values-en-rAU/strings.xml
+++ b/packages/SettingsLib/res/values-en-rAU/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging paused"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Charging rapidly"</string>
diff --git a/packages/SettingsLib/res/values-en-rCA/strings.xml b/packages/SettingsLib/res/values-en-rCA/strings.xml
index aac80ab..833a9e4 100644
--- a/packages/SettingsLib/res/values-en-rCA/strings.xml
+++ b/packages/SettingsLib/res/values-en-rCA/strings.xml
@@ -467,7 +467,7 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging paused"</string>
     <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
diff --git a/packages/SettingsLib/res/values-en-rGB/strings.xml b/packages/SettingsLib/res/values-en-rGB/strings.xml
index 208dbfa..3c98c25 100644
--- a/packages/SettingsLib/res/values-en-rGB/strings.xml
+++ b/packages/SettingsLib/res/values-en-rGB/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging paused"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Charging rapidly"</string>
diff --git a/packages/SettingsLib/res/values-en-rIN/strings.xml b/packages/SettingsLib/res/values-en-rIN/strings.xml
index 208dbfa..3c98c25 100644
--- a/packages/SettingsLib/res/values-en-rIN/strings.xml
+++ b/packages/SettingsLib/res/values-en-rIN/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging paused"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Charging rapidly"</string>
diff --git a/packages/SettingsLib/res/values-en-rXC/strings.xml b/packages/SettingsLib/res/values-en-rXC/strings.xml
index bfd7e83..b856eb1 100644
--- a/packages/SettingsLib/res/values-en-rXC/strings.xml
+++ b/packages/SettingsLib/res/values-en-rXC/strings.xml
@@ -467,7 +467,7 @@
     <string name="power_charging" msgid="6727132649743436802">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‎‏‏‎‏‏‏‎‎‏‎‎‏‏‏‎‎‎‎‎‎‎‏‏‎‎‏‎‎‎‏‏‏‎‎‎‏‏‏‏‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‎‏‎‎‏‏‎<xliff:g id="LEVEL">%1$s</xliff:g>‎‏‎‎‏‏‏‎ - ‎‏‎‎‏‏‎<xliff:g id="STATE">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‏‏‎‏‎‎‎‎‎‎‏‎‏‎‏‎‏‏‎‏‏‏‎‎‏‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‏‎‏‎‎‎‎‏‎‎‎‎‏‏‎‎‏‎‎‏‏‎<xliff:g id="TIME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ left until full‎‏‎‎‏‎"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‎‏‎‏‎‎‎‎‏‎‎‎‎‎‎‎‎‏‏‎‏‏‏‏‎‏‏‏‎‏‎‏‏‏‎‏‎‎‎‎‎‏‎‏‏‏‏‏‎‎‏‏‏‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎<xliff:g id="LEVEL">%1$s</xliff:g>‎‏‎‎‏‏‏‎ - ‎‏‎‎‏‏‎<xliff:g id="TIME">%2$s</xliff:g>‎‏‎‎‏‏‏‎ left until full‎‏‎‎‏‎"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‎‎‎‎‎‏‎‏‎‎‏‏‏‎‎‏‏‎‎‎‎‏‏‎‎‏‎‏‎‏‎‏‏‏‎‎‏‎‏‏‏‏‏‎‏‎‏‎‎‏‎‏‎‎‏‎‎‏‏‎<xliff:g id="LEVEL">%1$s</xliff:g>‎‏‎‎‏‏‏‎ - Charging is paused‎‏‎‎‏‎"</string>
+    <string name="power_charging_limited" msgid="6732738149313642521">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‎‏‏‏‏‎‏‏‏‏‏‎‏‏‎‏‎‏‏‎‎‏‏‎‏‏‎‏‏‏‎‎‎‏‏‏‏‏‎‎‎‎‎‎‎‎‎‎‏‏‎‎‏‎‎‏‎‎‏‏‎<xliff:g id="LEVEL">%1$s</xliff:g>‎‏‎‎‏‏‏‎ - Charging paused‎‏‎‎‏‎"</string>
     <string name="power_charging_future_paused" msgid="6829683663982987290">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‎‏‏‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‎‎‎‎‎‏‏‎‏‎‎‎‏‎‎‏‏‎<xliff:g id="LEVEL">%1$s</xliff:g>‎‏‎‎‏‏‏‎ - Charging to ‎‏‎‎‏‏‎<xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎‏‎‏‏‏‎‏‏‏‎‏‎‎‏‎‏‏‎‎‏‎‏‎‏‏‏‎‏‎‏‎‎‎‎‏‎‏‎‏‏‏‎‏‏‏‏‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎Unknown‎‏‎‎‏‎"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‏‎‏‎‏‏‏‎‏‏‏‎‎‏‎‎‎‏‏‏‎‎‎‏‎‏‏‎‏‎‏‎‎‎‎‎‏‏‎‎‏‏‎‏‏‎‎‏‏‎‏‎Charging‎‏‎‎‏‎"</string>
diff --git a/packages/SettingsLib/res/values-es-rUS/strings.xml b/packages/SettingsLib/res/values-es-rUS/strings.xml
index c11d6eb0..78af70a 100644
--- a/packages/SettingsLib/res/values-es-rUS/strings.xml
+++ b/packages/SettingsLib/res/values-es-rUS/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> para completar"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> para completar"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Se pausó la carga"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Se pausó la carga"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Cargando hasta <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Desconocido"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Cargando"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Cargando rápidamente"</string>
diff --git a/packages/SettingsLib/res/values-es/strings.xml b/packages/SettingsLib/res/values-es/strings.xml
index 1ef351a..ea3047b 100644
--- a/packages/SettingsLib/res/values-es/strings.xml
+++ b/packages/SettingsLib/res/values-es/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> hasta la carga completa"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> hasta la carga completa"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Carga en pausa"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Carga pausada"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Cargando hasta <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Desconocido"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Cargando"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carga rápida"</string>
diff --git a/packages/SettingsLib/res/values-et/strings.xml b/packages/SettingsLib/res/values-et/strings.xml
index e156011..78ade67 100644
--- a/packages/SettingsLib/res/values-et/strings.xml
+++ b/packages/SettingsLib/res/values-et/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Täislaadimiseks kulub <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – täislaadimiseks kulub <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – laadimine on peatatud"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – laadimine peatati"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – laadimine tasemeni <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Tundmatu"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Laadimine"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Kiirlaadimine"</string>
diff --git a/packages/SettingsLib/res/values-eu/strings.xml b/packages/SettingsLib/res/values-eu/strings.xml
index e5f399c..748f991 100644
--- a/packages/SettingsLib/res/values-eu/strings.xml
+++ b/packages/SettingsLib/res/values-eu/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> guztiz kargatu arte"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> guztiz kargatu arte"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Kargatze-prozesua etenda dago"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Kargatze-prozesua pausatuta dago"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> arte kargatzen"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Ezezaguna"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Kargatzen"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Bizkor kargatzen"</string>
diff --git a/packages/SettingsLib/res/values-fa/strings.xml b/packages/SettingsLib/res/values-fa/strings.xml
index ccacfde..44e61ef 100644
--- a/packages/SettingsLib/res/values-fa/strings.xml
+++ b/packages/SettingsLib/res/values-fa/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - ‏<xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> تا شارژ کامل باقی مانده است"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> تا شارژ کامل باقی مانده است"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - شارژ موقتاً متوقف شده است"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - شارژ موقتاً متوقف شد"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - درحال شارژ تا <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"ناشناس"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"در حال شارژ شدن"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"درحال شارژ شدن سریع"</string>
diff --git a/packages/SettingsLib/res/values-fi/strings.xml b/packages/SettingsLib/res/values-fi/strings.xml
index 101dc67..2599eb3 100644
--- a/packages/SettingsLib/res/values-fi/strings.xml
+++ b/packages/SettingsLib/res/values-fi/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> kunnes täynnä"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> kunnes täynnä"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Lataus on keskeytetty"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Lataaminen keskeytetty"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Tavoite: <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Tuntematon"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Ladataan"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Nopea lataus"</string>
diff --git a/packages/SettingsLib/res/values-fr-rCA/strings.xml b/packages/SettingsLib/res/values-fr-rCA/strings.xml
index ff58a51..314e8d1 100644
--- a/packages/SettingsLib/res/values-fr-rCA/strings.xml
+++ b/packages/SettingsLib/res/values-fr-rCA/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> jusqu\'à la recharge complète"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> (<xliff:g id="TIME">%2$s</xliff:g> jusqu\'à la recharge complète)"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - La recharge est interrompue"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Recharge interrompue"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - En charge jusqu\'à <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Inconnu"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Charge en cours…"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Recharge rapide"</string>
diff --git a/packages/SettingsLib/res/values-fr/strings.xml b/packages/SettingsLib/res/values-fr/strings.xml
index f9725f6..5cab1de 100644
--- a/packages/SettingsLib/res/values-fr/strings.xml
+++ b/packages/SettingsLib/res/values-fr/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Chargée à 100 %% dans <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - chargée à 100 %% dans <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – La recharge est en pause"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Recharge interrompue"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Recharge jusqu\'à <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Inconnu"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Batterie en charge"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Charge rapide"</string>
diff --git a/packages/SettingsLib/res/values-gl/strings.xml b/packages/SettingsLib/res/values-gl/strings.xml
index 3ceb99c..52bdaf6 100644
--- a/packages/SettingsLib/res/values-gl/strings.xml
+++ b/packages/SettingsLib/res/values-gl/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> para completar a carga"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> (<xliff:g id="TIME">%2$s</xliff:g> para completar a carga)"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: a carga está en pausa"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g>: Carga en pausa"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>: Cargando ata o <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Descoñecido"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Cargando"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Cargando rapidamente"</string>
diff --git a/packages/SettingsLib/res/values-gu/strings.xml b/packages/SettingsLib/res/values-gu/strings.xml
index 1821546..5a875a7 100644
--- a/packages/SettingsLib/res/values-gu/strings.xml
+++ b/packages/SettingsLib/res/values-gu/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"પૂર્ણ ચાર્જ થવામાં <xliff:g id="TIME">%1$s</xliff:g> બાકી છે"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - પૂર્ણ ચાર્જ થવામાં <xliff:g id="TIME">%2$s</xliff:g> બાકી છે"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ચાર્જિંગ થોભાવવામાં આવ્યું છે"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ચાર્જિંગ થોભાવેલું છે"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> સુધી ચાર્જિંગ"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"અજાણ્યું"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"ચાર્જ થઈ રહ્યું છે"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ઝડપથી ચાર્જ થાય છે"</string>
diff --git a/packages/SettingsLib/res/values-hi/strings.xml b/packages/SettingsLib/res/values-hi/strings.xml
index 4b899bb..0a38d16 100644
--- a/packages/SettingsLib/res/values-hi/strings.xml
+++ b/packages/SettingsLib/res/values-hi/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> में बैटरी पूरी चार्ज हो जाएगी"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> में बैटरी पूरी चार्ज हो जाएगी"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्जिंग को रोका गया है"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्जिंग रोकी गई है"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> तक चार्ज किया जा रहा है"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"अज्ञात"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"चार्ज हो रही है"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"तेज़ चार्ज हो रही है"</string>
@@ -602,7 +601,7 @@
     <string name="guest_exit_clear_data_button" msgid="3425812652180679014">"मिटाएं"</string>
     <string name="guest_exit_save_data_button" msgid="3690974510644963547">"सेव करें"</string>
     <string name="guest_exit_button" msgid="5774985819191803960">"मेहमान मोड से बाहर निकलें"</string>
-    <string name="guest_reset_button" msgid="2515069346223503479">"मेहमान मोड के सेशन को रीसेट करें?"</string>
+    <string name="guest_reset_button" msgid="2515069346223503479">"मेहमान मोड के सेशन को रीसेट करें"</string>
     <string name="guest_exit_quick_settings_button" msgid="1912362095913765471">"मेहमान मोड से बाहर निकलें"</string>
     <string name="guest_notification_ephemeral" msgid="7263252466950923871">"बाहर निकलने पर, सारी गतिविधि मिट जाएगी"</string>
     <string name="guest_notification_non_ephemeral" msgid="6843799963012259330">"बाहर निकलने पर, गतिविधि को मिटाया या सेव किया जा सकता है"</string>
diff --git a/packages/SettingsLib/res/values-hr/strings.xml b/packages/SettingsLib/res/values-hr/strings.xml
index 46c8b90..03eba0f 100644
--- a/packages/SettingsLib/res/values-hr/strings.xml
+++ b/packages/SettingsLib/res/values-hr/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do napunjenosti"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do napunjenosti"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – punjenje je pauzirano"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – punjenje pauzirano"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – punjenje do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Nepoznato"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Punjenje"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Brzo punjenje"</string>
diff --git a/packages/SettingsLib/res/values-hu/strings.xml b/packages/SettingsLib/res/values-hu/strings.xml
index d74490f..04a3ac6 100644
--- a/packages/SettingsLib/res/values-hu/strings.xml
+++ b/packages/SettingsLib/res/values-hu/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> a teljes töltöttségig"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> a teljes töltöttségig"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – A töltés szünetel"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Töltés szüneteltetve"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Töltés eddig: <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Ismeretlen"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Töltés"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Gyorstöltés"</string>
diff --git a/packages/SettingsLib/res/values-hy/strings.xml b/packages/SettingsLib/res/values-hy/strings.xml
index c26f94f..13f969c 100644
--- a/packages/SettingsLib/res/values-hy/strings.xml
+++ b/packages/SettingsLib/res/values-hy/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> մինչև լրիվ լիցքավորումը"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> մինչև լրիվ լիցքավորումը"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Լիցքավորումը դադարեցված է"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Լիցքավորումը դադարեցվել է"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Լիցքավորում մինչև <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Անհայտ"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Լիցքավորում"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Արագ լիցքավորում"</string>
diff --git a/packages/SettingsLib/res/values-in/strings.xml b/packages/SettingsLib/res/values-in/strings.xml
index 203485f..650d50b 100644
--- a/packages/SettingsLib/res/values-in/strings.xml
+++ b/packages/SettingsLib/res/values-in/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> lagi sampai penuh"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> lagi sampai penuh"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Pengisian daya dijeda"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Pengisian daya dijeda"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Mengisi daya sampai <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Tidak diketahui"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Mengisi daya"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Mengisi daya cepat"</string>
diff --git a/packages/SettingsLib/res/values-is/strings.xml b/packages/SettingsLib/res/values-is/strings.xml
index 9185877..da576f5 100644
--- a/packages/SettingsLib/res/values-is/strings.xml
+++ b/packages/SettingsLib/res/values-is/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> fram að fullri hleðslu"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> fram að fullri hleðslu"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Hlé var gert á hleðslu"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Hlé gert á hleðslu"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - hleður upp að <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Óþekkt"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Í hleðslu"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Hröð hleðsla"</string>
diff --git a/packages/SettingsLib/res/values-it/strings.xml b/packages/SettingsLib/res/values-it/strings.xml
index 0cd9e1e..7042506 100644
--- a/packages/SettingsLib/res/values-it/strings.xml
+++ b/packages/SettingsLib/res/values-it/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> alla ricarica completa"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> alla ricarica completa"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ricarica in pausa"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ricarica in pausa"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ricarica fino a <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Sconosciuta"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"In carica"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Ricarica veloce"</string>
diff --git a/packages/SettingsLib/res/values-iw/strings.xml b/packages/SettingsLib/res/values-iw/strings.xml
index 6293105..143c99d 100644
--- a/packages/SettingsLib/res/values-iw/strings.xml
+++ b/packages/SettingsLib/res/values-iw/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g>‏ – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"הזמן הנותר לטעינה מלאה: <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – הזמן הנותר לטעינה מלאה: <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – הטעינה הושהתה"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – הטעינה מושהית"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – טעינה עד מצב של <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"לא ידוע"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"בטעינה"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"הסוללה נטענת מהר"</string>
@@ -514,7 +513,7 @@
     <string name="ims_reg_status_not_registered" msgid="2989287366045704694">"לא רשום"</string>
     <string name="status_unavailable" msgid="5279036186589861608">"לא זמין"</string>
     <string name="wifi_status_mac_randomized" msgid="466382542497832189">"‏כתובת ה-MAC אקראית"</string>
-    <string name="wifi_tether_connected_summary" msgid="5282919920463340158">"{count,plural, =0{אין מכשירים מחוברים}=1{מכשיר אחד מחובר}two{# מכשירים מחוברים}many{# מכשירים מחוברים}other{# מכשירים מחוברים}}"</string>
+    <string name="wifi_tether_connected_summary" msgid="5282919920463340158">"{count,plural, =0{אין מכשירים מחוברים}=1{מכשיר אחד מחובר}one{# מכשירים מחוברים}two{# מכשירים מחוברים}other{# מכשירים מחוברים}}"</string>
     <string name="accessibility_manual_zen_more_time" msgid="5141801092071134235">"יותר זמן."</string>
     <string name="accessibility_manual_zen_less_time" msgid="6828877595848229965">"פחות זמן."</string>
     <string name="cancel" msgid="5665114069455378395">"ביטול"</string>
diff --git a/packages/SettingsLib/res/values-ja/strings.xml b/packages/SettingsLib/res/values-ja/strings.xml
index 9a9d6ea..11a5258 100644
--- a/packages/SettingsLib/res/values-ja/strings.xml
+++ b/packages/SettingsLib/res/values-ja/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"完了まであと <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - 完了まであと <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 充電は一時停止中"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 充電を一時停止しています"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> まで充電"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"不明"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"充電中"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"急速充電中"</string>
diff --git a/packages/SettingsLib/res/values-ka/strings.xml b/packages/SettingsLib/res/values-ka/strings.xml
index 2fa2d80..d6c0f38 100644
--- a/packages/SettingsLib/res/values-ka/strings.xml
+++ b/packages/SettingsLib/res/values-ka/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"სრულ დატენვამდე დარჩენილია <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> — სრულ დატენვამდე დარჩენილია <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - დატენვა ᲨეᲩერებულია"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - დატენვა დაპაუზებულია"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – დატენვა შემდეგ ნიშნულამდე: <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"უცნობი"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"იტენება"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"სწრაფად იტენება"</string>
diff --git a/packages/SettingsLib/res/values-kk/strings.xml b/packages/SettingsLib/res/values-kk/strings.xml
index a332004..96f2595 100644
--- a/packages/SettingsLib/res/values-kk/strings.xml
+++ b/packages/SettingsLib/res/values-kk/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Толық зарядталғанға дейін <xliff:g id="TIME">%1$s</xliff:g> қалды."</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – толық зарядталғанға дейін <xliff:g id="TIME">%2$s</xliff:g> қалды."</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – зарядтау кідіртілді."</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Зарядтау кідіртілді"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> деңгейіне дейін зарядталады"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Белгісіз"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Зарядталуда"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Жылдам зарядталуда"</string>
diff --git a/packages/SettingsLib/res/values-km/strings.xml b/packages/SettingsLib/res/values-km/strings.xml
index 2d78295..db4afdf 100644
--- a/packages/SettingsLib/res/values-km/strings.xml
+++ b/packages/SettingsLib/res/values-km/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> ទៀតទើបពេញ"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - នៅសល់ <xliff:g id="TIME">%2$s</xliff:g> ទៀតទើបពេញ"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ការសាកថ្មត្រូវបានផ្អាក"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - បានផ្អាក​ការសាកថ្ម"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - កំពុង​សាកថ្ម​ឱ្យដល់កម្រិត <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"មិន​ស្គាល់"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"កំពុងសាក​ថ្ម"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"កំពុងសាកថ្មយ៉ាងឆាប់រហ័ស"</string>
diff --git a/packages/SettingsLib/res/values-kn/strings.xml b/packages/SettingsLib/res/values-kn/strings.xml
index 5db4b16..74c2c74 100644
--- a/packages/SettingsLib/res/values-kn/strings.xml
+++ b/packages/SettingsLib/res/values-kn/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> - ಸಮಯದಲ್ಲಿ ಪೂರ್ತಿ ಚಾರ್ಜ್ ಆಗುತ್ತದೆ"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> ಸಮಯದಲ್ಲಿ ಪೂರ್ತಿ ಚಾರ್ಜ್ ಆಗುತ್ತದೆ"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ಚಾರ್ಜಿಂಗ್ ಅನ್ನು ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ಚಾರ್ಜಿಂಗ್ ವಿರಾಮಗೊಂಡಿದೆ"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> ವರೆಗೆ ಚಾರ್ಜ್ ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"ಅಪರಿಚಿತ"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"ಚಾರ್ಜ್ ಆಗುತ್ತಿದೆ"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ವೇಗದ ಚಾರ್ಜಿಂಗ್"</string>
diff --git a/packages/SettingsLib/res/values-ko/strings.xml b/packages/SettingsLib/res/values-ko/strings.xml
index 30ab26c..7a8ce1a 100644
--- a/packages/SettingsLib/res/values-ko/strings.xml
+++ b/packages/SettingsLib/res/values-ko/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> 후 충전 완료"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> 후 충전 완료"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 충전 일시중지됨"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 충전 일시중지됨"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>까지 충전"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"알 수 없음"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"충전 중"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"고속 충전 중"</string>
diff --git a/packages/SettingsLib/res/values-ky/strings.xml b/packages/SettingsLib/res/values-ky/strings.xml
index baacfd7..534c94f 100644
--- a/packages/SettingsLib/res/values-ky/strings.xml
+++ b/packages/SettingsLib/res/values-ky/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> кийин толук кубатталат"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> кийин толук кубатталат"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Кубаттоо тындырылды"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Кубаттоо тындырылды"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> чейин кубаттоо"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Белгисиз"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Кубатталууда"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Ыкчам кубатталууда"</string>
diff --git a/packages/SettingsLib/res/values-lo/strings.xml b/packages/SettingsLib/res/values-lo/strings.xml
index 36755f7..f37a4ea 100644
--- a/packages/SettingsLib/res/values-lo/strings.xml
+++ b/packages/SettingsLib/res/values-lo/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"ຍັງເຫຼືອອີກ <xliff:g id="TIME">%1$s</xliff:g> ຈຶ່ງຈະສາກເຕັມ"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"ຍັງເຫຼືອອີກ <xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> ຈຶ່ງຈະສາກເຕັມ"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ການສາກໄຟຖືກຢຸດໄວ້ຊົ່ວຄາວ"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ຢຸດການສາກໄວ້ຊົ່ວຄາວແລ້ວ"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - ກຳລັງສາກຈົນເຖິງ <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"ບໍ່ຮູ້ຈັກ"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"ກຳລັງສາກໄຟ"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ກຳລັງສາກໄຟດ່ວນ"</string>
diff --git a/packages/SettingsLib/res/values-lt/strings.xml b/packages/SettingsLib/res/values-lt/strings.xml
index 50652ea..cdfa115 100644
--- a/packages/SettingsLib/res/values-lt/strings.xml
+++ b/packages/SettingsLib/res/values-lt/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Liko <xliff:g id="TIME">%1$s</xliff:g>, kol bus visiškai įkrauta"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – liko <xliff:g id="TIME">%2$s</xliff:g>, kol bus visiškai įkrauta"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – įkrovimas pristabdytas"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – įkrovimas pristabdytas"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – įkraunama iki <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Nežinomas"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Kraunasi..."</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Greitai įkraunama"</string>
diff --git a/packages/SettingsLib/res/values-lv/strings.xml b/packages/SettingsLib/res/values-lv/strings.xml
index c199fea..a5ecfff 100644
--- a/packages/SettingsLib/res/values-lv/strings.xml
+++ b/packages/SettingsLib/res/values-lv/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> — <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> līdz pilnai uzlādei"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> — <xliff:g id="TIME">%2$s</xliff:g> līdz pilnai uzlādei"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> — uzlāde ir pārtraukta"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Uzlāde ir apturēta"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>. Tiks uzlādēts līdz <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Nezināms"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Uzlāde"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Notiek ātrā uzlāde"</string>
diff --git a/packages/SettingsLib/res/values-mk/strings.xml b/packages/SettingsLib/res/values-mk/strings.xml
index 957f68b..a41d953 100644
--- a/packages/SettingsLib/res/values-mk/strings.xml
+++ b/packages/SettingsLib/res/values-mk/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> до полна батерија"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> до полна батерија"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Полнењето е паузирано"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Полнењето е паузирано"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Се полни на <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Непознато"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Се полни"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Брзо полнење"</string>
diff --git a/packages/SettingsLib/res/values-ml/strings.xml b/packages/SettingsLib/res/values-ml/strings.xml
index 1cc92ca..83b87f8 100644
--- a/packages/SettingsLib/res/values-ml/strings.xml
+++ b/packages/SettingsLib/res/values-ml/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"പൂർണ്ണമാകാൻ <xliff:g id="TIME">%1$s</xliff:g> ശേഷിക്കുന്നു"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - പൂർണ്ണമാകാൻ <xliff:g id="TIME">%2$s</xliff:g> ശേഷിക്കുന്നു"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ചാർജിംഗ് താൽക്കാലികമായി നിർത്തി"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ചാർജ് ചെയ്യൽ താൽക്കാലികമായി നിർത്തി"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> വരെ ചാർജ് ചെയ്യുന്നു"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"അജ്ഞാതം"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"ചാർജ് ചെയ്യുന്നു"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"അതിവേഗ ചാർജിംഗ്"</string>
diff --git a/packages/SettingsLib/res/values-mn/strings.xml b/packages/SettingsLib/res/values-mn/strings.xml
index a107c70..b77d8fb 100644
--- a/packages/SettingsLib/res/values-mn/strings.xml
+++ b/packages/SettingsLib/res/values-mn/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Дүүрэх хүртэл <xliff:g id="TIME">%1$s</xliff:g> үлдсэн"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - дүүрэх хүртэл <xliff:g id="TIME">%2$s</xliff:g> үлдсэн"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Цэнэглэхийг түр зогсоосон"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Цэнэглэлтийг түр зогсоосон"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> руу цэнэглэж байна"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Тодорхойгүй"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Цэнэглэж байна"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Хурдан цэнэглэж байна"</string>
diff --git a/packages/SettingsLib/res/values-mr/strings.xml b/packages/SettingsLib/res/values-mr/strings.xml
index d5c83a8..9d91684 100644
--- a/packages/SettingsLib/res/values-mr/strings.xml
+++ b/packages/SettingsLib/res/values-mr/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"पूर्ण चार्ज होण्यासाठी <xliff:g id="TIME">%1$s</xliff:g> शिल्लक आहेत"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - पूर्ण चार्ज होण्यासाठी <xliff:g id="TIME">%2$s</xliff:g> शिल्लक आहे"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्ज करणे थांबवले आहे"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्ज करणे थांबवले"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> वर चार्ज करत आहे"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"अज्ञात"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"चार्ज होत आहे"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"वेगाने चार्ज होत आहे"</string>
diff --git a/packages/SettingsLib/res/values-ms/strings.xml b/packages/SettingsLib/res/values-ms/strings.xml
index ae8be8e..d302214 100644
--- a/packages/SettingsLib/res/values-ms/strings.xml
+++ b/packages/SettingsLib/res/values-ms/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> lagi sebelum penuh"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> lagi sebelum penuh"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Pengecasan dijeda"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Pengecasan dijeda"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Mengecas kepada <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Tidak diketahui"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Mengecas"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Mengecas dgn cepat"</string>
diff --git a/packages/SettingsLib/res/values-my/strings.xml b/packages/SettingsLib/res/values-my/strings.xml
index f16cdca..1e26b08 100644
--- a/packages/SettingsLib/res/values-my/strings.xml
+++ b/packages/SettingsLib/res/values-my/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"အားပြည့်ရန် <xliff:g id="TIME">%1$s</xliff:g> လိုသည်"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"အားပြည့်ရန် <xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> လိုသည်"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - အားသွင်းခြင်းကို ခဏရပ်ထားသည်"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - အားသွင်းမှု ခဏရပ်ထားသည်"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> အထိ အားသွင်းရန်"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"မသိ"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"အားသွင်းနေပါသည်"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"အမြန် အားသွင်းနေသည်"</string>
diff --git a/packages/SettingsLib/res/values-nb/strings.xml b/packages/SettingsLib/res/values-nb/strings.xml
index f7a10a6..be52597 100644
--- a/packages/SettingsLib/res/values-nb/strings.xml
+++ b/packages/SettingsLib/res/values-nb/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Fulladet om <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – Fulladet om <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ladingen er satt på pause"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ladingen er satt på pause"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – lader til <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Ukjent"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Lader"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Lader raskt"</string>
diff --git a/packages/SettingsLib/res/values-ne/strings.xml b/packages/SettingsLib/res/values-ne/strings.xml
index a966f15..4d2eb2e 100644
--- a/packages/SettingsLib/res/values-ne/strings.xml
+++ b/packages/SettingsLib/res/values-ne/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"पूरा चार्ज हुन <xliff:g id="TIME">%1$s</xliff:g> लाग्ने छ"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - पूरा चार्ज हुन <xliff:g id="TIME">%2$s</xliff:g> लाग्ने छ"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्ज गर्ने प्रक्रिया रोकिएको छ"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्ज गर्ने प्रक्रिया पज गरिएको छ"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> सम्म चार्ज हुने छ"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"अज्ञात"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"चार्ज हुँदै छ"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"द्रुत गतिमा चार्ज गरिँदै छ"</string>
diff --git a/packages/SettingsLib/res/values-nl/strings.xml b/packages/SettingsLib/res/values-nl/strings.xml
index c5406ee..898da11 100644
--- a/packages/SettingsLib/res/values-nl/strings.xml
+++ b/packages/SettingsLib/res/values-nl/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Vol over <xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - vol over <xliff:g id="TIME">%2$s</xliff:g>"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Opladen is onderbroken"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Opladen onderbroken"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Opladen tot <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Onbekend"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Opladen"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Snel opladen"</string>
diff --git a/packages/SettingsLib/res/values-or/strings.xml b/packages/SettingsLib/res/values-or/strings.xml
index 37cc25e..6b6752f 100644
--- a/packages/SettingsLib/res/values-or/strings.xml
+++ b/packages/SettingsLib/res/values-or/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"ପୂର୍ଣ୍ଣ ହେବାକୁ ଆଉ <xliff:g id="TIME">%1$s</xliff:g> ବାକି ଅଛି"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - ପୂର୍ଣ୍ଣ ହେବାକୁ ଆଉ <xliff:g id="TIME">%2$s</xliff:g> ବାକି ଅଛି"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ଚାର୍ଜିଂକୁ ବିରତ କରାଯାଇଛି"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ଚାର୍ଜିଂ ବିରତ କରାଯାଇଛି"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> ପର୍ଯ୍ୟନ୍ତ ଚାର୍ଜ ହେଉଛି"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"ଅଜ୍ଞାତ"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"ଚାର୍ଜ ହେଉଛି"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ଶୀଘ୍ର ଚାର୍ଜ ହେଉଛି"</string>
diff --git a/packages/SettingsLib/res/values-pa/strings.xml b/packages/SettingsLib/res/values-pa/strings.xml
index 767cbc5..896b5ec 100644
--- a/packages/SettingsLib/res/values-pa/strings.xml
+++ b/packages/SettingsLib/res/values-pa/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"ਬੈਟਰੀ ਪੂਰੀ ਚਾਰਜ ਹੋਣ ਵਿੱਚ <xliff:g id="TIME">%1$s</xliff:g> ਬਾਕੀ"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - ਬੈਟਰੀ ਪੂਰੀ ਚਾਰਜ ਹੋਣ ਵਿੱਚ <xliff:g id="TIME">%2$s</xliff:g> ਬਾਕੀ"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ਚਾਰਜਿੰਗ ਨੂੰ ਰੋਕਿਆ ਗਿਆ ਹੈ"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ਬੈਟਰੀ ਦੀ ਚਾਰਜਿੰਗ ਨੂੰ ਰੋਕਿਆ ਗਿਆ"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> ਤੱਕ ਚਾਰਜ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"ਅਗਿਆਤ"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"ਚਾਰਜ ਹੋ ਰਹੀ ਹੈ"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ਤੇਜ਼ ਚਾਰਜ ਹੋ ਰਹੀ ਹੈ"</string>
diff --git a/packages/SettingsLib/res/values-pl/strings.xml b/packages/SettingsLib/res/values-pl/strings.xml
index 48bc82e..263e0d7 100644
--- a/packages/SettingsLib/res/values-pl/strings.xml
+++ b/packages/SettingsLib/res/values-pl/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do pełnego naładowania"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do pełnego naładowania"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – ładowanie zostało wstrzymane"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – wstrzymano ładowanie"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Ładuję do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Nieznane"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Ładowanie"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Szybkie ładowanie"</string>
diff --git a/packages/SettingsLib/res/values-pt-rBR/strings.xml b/packages/SettingsLib/res/values-pt-rBR/strings.xml
index 68a1c49..e107064 100644
--- a/packages/SettingsLib/res/values-pt-rBR/strings.xml
+++ b/packages/SettingsLib/res/values-pt-rBR/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> até a conclusão"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> até a conclusão"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: o carregamento está pausado"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g>: carregamento pausado"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>: carregando até <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Desconhecido"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Carregando"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carregando rápido"</string>
diff --git a/packages/SettingsLib/res/values-pt-rPT/strings.xml b/packages/SettingsLib/res/values-pt-rPT/strings.xml
index 87bab50..165ecc4 100644
--- a/packages/SettingsLib/res/values-pt-rPT/strings.xml
+++ b/packages/SettingsLib/res/values-pt-rPT/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> até à carga máxima"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> até à carga máxima"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – O carregamento está pausado"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Carregamento em pausa"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – A carregar até <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Desconhecido"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"A carregar"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carregamento rápido"</string>
diff --git a/packages/SettingsLib/res/values-pt/strings.xml b/packages/SettingsLib/res/values-pt/strings.xml
index 68a1c49..e107064 100644
--- a/packages/SettingsLib/res/values-pt/strings.xml
+++ b/packages/SettingsLib/res/values-pt/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> até a conclusão"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> até a conclusão"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: o carregamento está pausado"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g>: carregamento pausado"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>: carregando até <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Desconhecido"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Carregando"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carregando rápido"</string>
diff --git a/packages/SettingsLib/res/values-ro/strings.xml b/packages/SettingsLib/res/values-ro/strings.xml
index f81ef4c..44ec043 100644
--- a/packages/SettingsLib/res/values-ro/strings.xml
+++ b/packages/SettingsLib/res/values-ro/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> până la finalizare"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> până la finalizare"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Încărcarea este întreruptă"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Încărcarea s-a întrerupt"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Se încarcă până la <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Necunoscut"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Se încarcă"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Se încarcă rapid"</string>
diff --git a/packages/SettingsLib/res/values-ru/strings.xml b/packages/SettingsLib/res/values-ru/strings.xml
index 4dcee13..102cf1b 100644
--- a/packages/SettingsLib/res/values-ru/strings.xml
+++ b/packages/SettingsLib/res/values-ru/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> до полной зарядки"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> до полной зарядки"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: зарядка приостановлена"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – зарядка приостановлена"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – заряжается до <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Неизвестно"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Идет зарядка"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Быстрая зарядка"</string>
diff --git a/packages/SettingsLib/res/values-si/strings.xml b/packages/SettingsLib/res/values-si/strings.xml
index 17c59ea..a8963af 100644
--- a/packages/SettingsLib/res/values-si/strings.xml
+++ b/packages/SettingsLib/res/values-si/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"සම්පූර්ණ වීමට <xliff:g id="TIME">%1$s</xliff:g>ක් ඉතිරියි"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - සම්පූර්ණ වීමට <xliff:g id="TIME">%2$s</xliff:g>ක් ඉතිරියි"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ආරෝපණය විරාම කර ඇත"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ආරෝපණය විරාම කළා"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> වෙත ආරෝපණය"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"නොදනී"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"ආරෝපණය වෙමින්"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ශීඝ්‍ර ආරෝපණය"</string>
diff --git a/packages/SettingsLib/res/values-sk/strings.xml b/packages/SettingsLib/res/values-sk/strings.xml
index d9ce6cf..d7bdd89 100644
--- a/packages/SettingsLib/res/values-sk/strings.xml
+++ b/packages/SettingsLib/res/values-sk/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do úplného nabitia"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do úplného nabitia"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Nabíjanie je pozastavené"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – nabíjanie bolo pozastavené"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – nabíja sa na úroveň <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Neznáme"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Nabíja sa"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Rýchle nabíjanie"</string>
diff --git a/packages/SettingsLib/res/values-sl/strings.xml b/packages/SettingsLib/res/values-sl/strings.xml
index 8f9b059..6cb5b3c 100644
--- a/packages/SettingsLib/res/values-sl/strings.xml
+++ b/packages/SettingsLib/res/values-sl/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Še <xliff:g id="TIME">%1$s</xliff:g> do napolnjenosti"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – še <xliff:g id="TIME">%2$s</xliff:g> do napolnjenosti"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Polnjenje je začasno zaustavljeno"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – polnjenje je začasno zaustavljeno"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – polnjenje do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Neznano"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Polnjenje"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Hitro polnjenje"</string>
diff --git a/packages/SettingsLib/res/values-sq/strings.xml b/packages/SettingsLib/res/values-sq/strings.xml
index d467eea..54a8361 100644
--- a/packages/SettingsLib/res/values-sq/strings.xml
+++ b/packages/SettingsLib/res/values-sq/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> derisa të mbushet"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> derisa të mbushet"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Karikimi është vendosur në pauzë"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Karikimi në pauzë"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Po karikohet deri në <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"I panjohur"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Po karikohet"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Karikim i shpejtë"</string>
diff --git a/packages/SettingsLib/res/values-sr/strings.xml b/packages/SettingsLib/res/values-sr/strings.xml
index 9cc43d9..d61938b 100644
--- a/packages/SettingsLib/res/values-sr/strings.xml
+++ b/packages/SettingsLib/res/values-sr/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> до краја пуњења"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> до краја пуњења"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Пуњење је заустављено"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Пуњење је паузирано"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – пуњење до <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Непознато"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Пуни се"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Брзо се пуни"</string>
diff --git a/packages/SettingsLib/res/values-sv/strings.xml b/packages/SettingsLib/res/values-sv/strings.xml
index 89deafa..3ba4e9b 100644
--- a/packages/SettingsLib/res/values-sv/strings.xml
+++ b/packages/SettingsLib/res/values-sv/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> kvar tills fulladdat"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> kvar tills fulladdat"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Laddningen har pausats"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laddningen har pausats"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laddar till <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Okänd"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Laddar"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Laddas snabbt"</string>
diff --git a/packages/SettingsLib/res/values-sw/strings.xml b/packages/SettingsLib/res/values-sw/strings.xml
index 1fa5edd..c167697 100644
--- a/packages/SettingsLib/res/values-sw/strings.xml
+++ b/packages/SettingsLib/res/values-sw/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Zimesalia <xliff:g id="TIME">%1$s</xliff:g> ijae chaji"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> zimesalia ijae chaji"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Imesitisha kuchaji"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Imesitisha kuchaji"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Itachaji hadi <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Haijulikani"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Inachaji"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Inachaji kwa kasi"</string>
diff --git a/packages/SettingsLib/res/values-ta/strings.xml b/packages/SettingsLib/res/values-ta/strings.xml
index 1b7b643..57661ba 100644
--- a/packages/SettingsLib/res/values-ta/strings.xml
+++ b/packages/SettingsLib/res/values-ta/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"முழுவதும் சார்ஜாக <xliff:g id="TIME">%1$s</xliff:g> ஆகும்"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - முழுவதும் சார்ஜாக <xliff:g id="TIME">%2$s</xliff:g> ஆகும்"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - சார்ஜ் ஏறுவது இடைநிறுத்தப்பட்டுள்ளது"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - சார்ஜிங் இடைநிறுத்தப்பட்டது"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> வரை சார்ஜ் செய்யப்படும்"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"அறியப்படாத"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"சார்ஜ் ஆகிறது"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"வேகமாக சார்ஜாகிறது"</string>
diff --git a/packages/SettingsLib/res/values-te/strings.xml b/packages/SettingsLib/res/values-te/strings.xml
index 27e9dbd..365a8f2 100644
--- a/packages/SettingsLib/res/values-te/strings.xml
+++ b/packages/SettingsLib/res/values-te/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g>లో పూర్తిగా ఛార్జ్ అవుతుంది"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g>లో పూర్తిగా ఛార్జ్ అవుతుంది"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ఛార్జింగ్ పాజ్ చేయబడింది"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ఛార్జింగ్ పాజ్ చేయబడింది"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> ఛార్జింగ్"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"తెలియదు"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"ఛార్జ్ అవుతోంది"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"వేగవంతమైన ఛార్జింగ్"</string>
diff --git a/packages/SettingsLib/res/values-th/strings.xml b/packages/SettingsLib/res/values-th/strings.xml
index 6b9c077..cea4dd2 100644
--- a/packages/SettingsLib/res/values-th/strings.xml
+++ b/packages/SettingsLib/res/values-th/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"อีก <xliff:g id="TIME">%1$s</xliff:g>จึงจะเต็ม"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - อีก <xliff:g id="TIME">%2$s</xliff:g> จึงจะเต็ม"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - การชาร์จหยุดชั่วคราว"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - หยุดชาร์จชั่วคราว"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - กำลังชาร์จจนถึง <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"ไม่ทราบ"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"กำลังชาร์จ"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"กำลังชาร์จอย่างเร็ว"</string>
diff --git a/packages/SettingsLib/res/values-tl/strings.xml b/packages/SettingsLib/res/values-tl/strings.xml
index fddc1d0..e4503a0 100644
--- a/packages/SettingsLib/res/values-tl/strings.xml
+++ b/packages/SettingsLib/res/values-tl/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> na lang bago mapuno"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> na lang bago mapuno"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Naka-pause ang pag-charge"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Na-pause ang pag-charge"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - China-charge hanggang <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Hindi Kilala"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Nagcha-charge"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Mabilis na charge"</string>
diff --git a/packages/SettingsLib/res/values-tr/strings.xml b/packages/SettingsLib/res/values-tr/strings.xml
index 31dcedb8..eada480 100644
--- a/packages/SettingsLib/res/values-tr/strings.xml
+++ b/packages/SettingsLib/res/values-tr/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Tamamen şarj olmasına <xliff:g id="TIME">%1$s</xliff:g> kaldı"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - Tamamen şarj olmasına <xliff:g id="TIME">%2$s</xliff:g> kaldı"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: Şarj işlemi duraklatıldı"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Şarj işlemi duraklatıldı"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> seviyesine kadar şarj ediliyor"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Bilinmiyor"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Şarj oluyor"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Hızlı şarj oluyor"</string>
diff --git a/packages/SettingsLib/res/values-uk/strings.xml b/packages/SettingsLib/res/values-uk/strings.xml
index 2fdc227..8ab8db7 100644
--- a/packages/SettingsLib/res/values-uk/strings.xml
+++ b/packages/SettingsLib/res/values-uk/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> до повного заряду"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> до повного заряду"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Заряджання призупинено"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – заряджання призупинено"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Заряджання до <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Невідомо"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Заряджається"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Швидке заряджання"</string>
diff --git a/packages/SettingsLib/res/values-ur/strings.xml b/packages/SettingsLib/res/values-ur/strings.xml
index 82dbdb3..ebc6edc 100644
--- a/packages/SettingsLib/res/values-ur/strings.xml
+++ b/packages/SettingsLib/res/values-ur/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"‎<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>‎"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"مکمل چارج ہونے میں <xliff:g id="TIME">%1$s</xliff:g> باقی ہے"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"مکمل چارج ہونے میں <xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> باقی ہے"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - چارجنگ موقوف ہے"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - چارجنگ موقوف کی گئی"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> چارج کیا جائے گا"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"نامعلوم"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"چارج ہو رہا ہے"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"تیزی سے چارج ہو رہا ہے"</string>
diff --git a/packages/SettingsLib/res/values-uz/strings.xml b/packages/SettingsLib/res/values-uz/strings.xml
index ab997b6..19601dd 100644
--- a/packages/SettingsLib/res/values-uz/strings.xml
+++ b/packages/SettingsLib/res/values-uz/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Toʻlishiga <xliff:g id="TIME">%1$s</xliff:g> qoldi"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – Toʻlishiga <xliff:g id="TIME">%2$s</xliff:g> qoldi"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Quvvatlash pauza qilindi"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Quvvatlash pauzada"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>, <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> gacha quvvat oladi"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Noma’lum"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Quvvat olmoqda"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Tezkor quvvat olmoqda"</string>
diff --git a/packages/SettingsLib/res/values-vi/strings.xml b/packages/SettingsLib/res/values-vi/strings.xml
index 44c820a5..f900480 100644
--- a/packages/SettingsLib/res/values-vi/strings.xml
+++ b/packages/SettingsLib/res/values-vi/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> nữa là pin đầy"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> nữa là pin đầy"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Đã tạm dừng sạc"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Đã tạm dừng sạc"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Sạc đến <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Không xác định"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Đang sạc"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Đang sạc nhanh"</string>
diff --git a/packages/SettingsLib/res/values-zh-rCN/strings.xml b/packages/SettingsLib/res/values-zh-rCN/strings.xml
index eae1c39..6909f22 100644
--- a/packages/SettingsLib/res/values-zh-rCN/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rCN/strings.xml
@@ -141,7 +141,7 @@
     <string name="bluetooth_pairing_decline" msgid="6483118841204885890">"取消"</string>
     <string name="bluetooth_pairing_will_share_phonebook" msgid="3064334458659165176">"配对之后,所配对的设备将可以在建立连接后访问您的通讯录和通话记录。"</string>
     <string name="bluetooth_pairing_error_message" msgid="6626399020672335565">"无法与“<xliff:g id="DEVICE_NAME">%1$s</xliff:g>”进行配对。"</string>
-    <string name="bluetooth_pairing_pin_error_message" msgid="264422127613704940">"PIN 码或密钥不正确,因此无法与“<xliff:g id="DEVICE_NAME">%1$s</xliff:g>”配对。"</string>
+    <string name="bluetooth_pairing_pin_error_message" msgid="264422127613704940">"PIN 码或通行密钥不正确,因此无法与“<xliff:g id="DEVICE_NAME">%1$s</xliff:g>”配对。"</string>
     <string name="bluetooth_pairing_device_down_error_message" msgid="2554424863101358857">"无法与“<xliff:g id="DEVICE_NAME">%1$s</xliff:g>”进行通信。"</string>
     <string name="bluetooth_pairing_rejected_error_message" msgid="5943444352777314442">"<xliff:g id="DEVICE_NAME">%1$s</xliff:g> 已拒绝配对。"</string>
     <string name="bluetooth_talkback_computer" msgid="3736623135703893773">"计算机"</string>
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"还需<xliff:g id="TIME">%1$s</xliff:g>充满"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - 还需<xliff:g id="TIME">%2$s</xliff:g>充满"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 充电已暂停"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暂停充电"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - 正在充到 <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"未知"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"正在充电"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"正在快速充电"</string>
diff --git a/packages/SettingsLib/res/values-zh-rHK/strings.xml b/packages/SettingsLib/res/values-zh-rHK/strings.xml
index 4371bb3..2d59d2d 100644
--- a/packages/SettingsLib/res/values-zh-rHK/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rHK/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g>後充滿電"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g>後充滿電"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暫停充電"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暫停充電"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - 正在充電至 <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"未知"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"充電中"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"快速充電中"</string>
diff --git a/packages/SettingsLib/res/values-zh-rTW/strings.xml b/packages/SettingsLib/res/values-zh-rTW/strings.xml
index d76a4a4..40b42fc 100644
--- a/packages/SettingsLib/res/values-zh-rTW/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rTW/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g>後充飽"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g>後充飽"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暫停充電"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暫停充電"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - 正在充電至 <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"不明"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"充電中"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"快速充電中"</string>
diff --git a/packages/SettingsLib/res/values-zu/strings.xml b/packages/SettingsLib/res/values-zu/strings.xml
index 4dc7651..a73f067 100644
--- a/packages/SettingsLib/res/values-zu/strings.xml
+++ b/packages/SettingsLib/res/values-zu/strings.xml
@@ -467,9 +467,8 @@
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> okusele kuze kugcwale"</string>
     <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> okusele kuze kugcwale"</string>
-    <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ukushaja kumisiwe okwesikhashana"</string>
-    <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
-    <skip />
+    <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ukushaja kumiswe isikhashana"</string>
+    <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ishaja ku-<xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Akwaziwa"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Iyashaja"</string>
     <string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Ishaja ngokushesha"</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
index 1573edb..5610ac4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
@@ -17,6 +17,7 @@
 package com.android.settingslib;
 
 import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE;
+import static android.app.admin.DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY;
 import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
 
 import android.annotation.NonNull;
@@ -733,6 +734,26 @@
     }
 
     /**
+     * Checks whether MTE (Advanced memory protection) controls are disabled by the enterprise
+     * policy.
+     */
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static EnforcedAdmin checkIfMteIsDisabled(Context context) {
+        final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
+        if (dpm.getMtePolicy() == MTE_NOT_CONTROLLED_BY_POLICY) {
+            return null;
+        }
+        EnforcedAdmin admin =
+                RestrictedLockUtils.getProfileOrDeviceOwner(
+                        context, UserHandle.of(UserHandle.USER_SYSTEM));
+        if (admin != null) {
+            return admin;
+        }
+        int profileId = getManagedProfileId(context, UserHandle.USER_SYSTEM);
+        return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId));
+    }
+
+    /**
      * Show restricted setting dialog.
      */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index f4355c3..3e63052 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -161,10 +161,7 @@
      * @return {@code true} if successfully call, otherwise return {@code false}
      */
     public boolean connectDevice(MediaDevice connectDevice) {
-        MediaDevice device = null;
-        synchronized (mMediaDevicesLock) {
-            device = getMediaDeviceById(mMediaDevices, connectDevice.getId());
-        }
+        MediaDevice device = getMediaDeviceById(connectDevice.getId());
         if (device == null) {
             Log.w(TAG, "connectDevice() connectDevice not in the list!");
             return false;
@@ -277,23 +274,6 @@
     /**
      * Find the MediaDevice through id.
      *
-     * @param devices the list of MediaDevice
-     * @param id the unique id of MediaDevice
-     * @return MediaDevice
-     */
-    public MediaDevice getMediaDeviceById(List<MediaDevice> devices, String id) {
-        for (MediaDevice mediaDevice : devices) {
-            if (TextUtils.equals(mediaDevice.getId(), id)) {
-                return mediaDevice;
-            }
-        }
-        Log.i(TAG, "getMediaDeviceById() can't found device");
-        return null;
-    }
-
-    /**
-     * Find the MediaDevice from all media devices by id.
-     *
      * @param id the unique id of MediaDevice
      * @return MediaDevice
      */
@@ -305,7 +285,7 @@
                 }
             }
         }
-        Log.i(TAG, "Unable to find device " + id);
+        Log.i(TAG, "getMediaDeviceById() failed to find device with id: " + id);
         return null;
     }
 
@@ -672,10 +652,7 @@
 
         @Override
         public void onConnectedDeviceChanged(String id) {
-            MediaDevice connectDevice = null;
-            synchronized (mMediaDevicesLock) {
-                connectDevice = getMediaDeviceById(mMediaDevices, id);
-            }
+            MediaDevice connectDevice = getMediaDeviceById(id);
             connectDevice = connectDevice != null
                     ? connectDevice : updateCurrentConnectedDevice();
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index 24bb1bc..3ec0ba6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -206,8 +206,7 @@
         when(device1.getId()).thenReturn(TEST_DEVICE_ID_1);
         when(device2.getId()).thenReturn(TEST_DEVICE_ID_2);
 
-        final MediaDevice device = mLocalMediaManager
-                .getMediaDeviceById(mLocalMediaManager.mMediaDevices, TEST_DEVICE_ID_2);
+        MediaDevice device = mLocalMediaManager.getMediaDeviceById(TEST_DEVICE_ID_2);
 
         assertThat(device.getId()).isEqualTo(TEST_DEVICE_ID_2);
     }
@@ -222,8 +221,7 @@
         when(device1.getId()).thenReturn(TEST_DEVICE_ID_1);
         when(device2.getId()).thenReturn(TEST_DEVICE_ID_2);
 
-        final MediaDevice device = mLocalMediaManager
-                .getMediaDeviceById(mLocalMediaManager.mMediaDevices, TEST_CURRENT_DEVICE_ID);
+        MediaDevice device = mLocalMediaManager.getMediaDeviceById(TEST_CURRENT_DEVICE_ID);
 
         assertThat(device).isNull();
     }
@@ -238,12 +236,7 @@
         when(device1.getId()).thenReturn(null);
         when(device2.getId()).thenReturn(null);
 
-        MediaDevice device = mLocalMediaManager
-                .getMediaDeviceById(mLocalMediaManager.mMediaDevices, TEST_CURRENT_DEVICE_ID);
-
-        assertThat(device).isNull();
-
-        device = mLocalMediaManager.getMediaDeviceById(TEST_CURRENT_DEVICE_ID);
+        MediaDevice device = mLocalMediaManager.getMediaDeviceById(TEST_CURRENT_DEVICE_ID);
 
         assertThat(device).isNull();
     }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 267c196..e59ba5a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -168,6 +168,11 @@
         Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT,
         Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT,
         Settings.Secure.NAVIGATION_MODE,
+        Settings.Secure.TRACKPAD_GESTURE_BACK_ENABLED,
+        Settings.Secure.TRACKPAD_GESTURE_HOME_ENABLED,
+        Settings.Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED,
+        Settings.Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED,
+        Settings.Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED,
         Settings.Secure.SKIP_GESTURE_COUNT,
         Settings.Secure.SKIP_TOUCH_COUNT,
         Settings.Secure.SILENCE_ALARMS_GESTURE_COUNT,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index def9c19..58ec349 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -255,6 +255,11 @@
                 new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
         VALIDATORS.put(Secure.BACK_GESTURE_INSET_SCALE_RIGHT,
                 new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
+        VALIDATORS.put(Secure.TRACKPAD_GESTURE_BACK_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.TRACKPAD_GESTURE_HOME_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.AWARE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SKIP_GESTURE_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.SKIP_TOUCH_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR);
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 48114ba1e..4365a9b 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -425,6 +425,7 @@
                     Settings.Global.RESTRICTED_NETWORKING_MODE,
                     Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT,
                     Settings.Global.SAFE_BOOT_DISALLOWED,
+                    Settings.Global.SECURE_FRP_MODE,
                     Settings.Global.SELINUX_STATUS,
                     Settings.Global.SELINUX_UPDATE_CONTENT_URL,
                     Settings.Global.SELINUX_UPDATE_METADATA_URL,
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index 6f7d20a..68679c79 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -455,7 +455,8 @@
         intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_HASH, bugreportHash);
         intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_NONCE, nonce);
         intent.putExtra(EXTRA_BUGREPORT, bugreportFileName);
-        context.sendBroadcast(intent, android.Manifest.permission.DUMP);
+        context.sendBroadcastAsUser(intent, UserHandle.SYSTEM,
+                android.Manifest.permission.DUMP);
     }
 
     /**
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 4538179..6984b5a 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -411,6 +411,7 @@
 
         <service android:name=".screenshot.ScreenshotCrossProfileService"
                  android:permission="com.android.systemui.permission.SELF"
+                 android:process=":screenshot_cross_profile"
                  android:exported="false" />
 
         <service android:name=".screenrecord.RecordingService" />
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index aaee42f..f92625b 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -33,6 +33,18 @@
       ]
     },
     {
+      // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+)
+      "name": "SystemUIGoogleBiometricsScreenshotTests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
       // Permission indicators
       "name": "CtsPermission4TestCases",
       "options": [
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 c540f0f..e138ef8 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
@@ -20,6 +20,7 @@
 import android.icu.text.NumberFormat
 import android.util.TypedValue
 import android.view.LayoutInflater
+import android.view.View
 import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.customization.R
@@ -151,9 +152,15 @@
         view: AnimatableClockView,
     ) : DefaultClockFaceController(view) {
         override fun recomputePadding(targetRegion: Rect?) {
-            // Ignore Target Region until top padding fixed in aod
+            // We center the view within the targetRegion instead of within the parent
+            // view by computing the difference and adding that to the padding.
+            val parent = view.parent
+            val yDiff =
+                if (targetRegion != null && parent is View && parent.isLaidOut())
+                    targetRegion.centerY() - parent.height / 2f
+                else 0f
             val lp = view.getLayoutParams() as FrameLayout.LayoutParams
-            lp.topMargin = (-0.5f * view.bottom).toInt()
+            lp.topMargin = (-0.5f * view.bottom + yDiff).toInt()
             view.setLayoutParams(lp)
         }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
new file mode 100644
index 0000000..18e8a96
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.shared.quickaffordance.shared.model
+
+object KeyguardQuickAffordancePreviewConstants {
+    const val MESSAGE_ID_SLOT_SELECTED = 1337
+    const val KEY_SLOT_ID = "slot_id"
+    const val KEY_INITIALLY_SELECTED_SLOT_ID = "initially_selected_slot_id"
+}
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index 218c5cc..b49afee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -35,7 +35,6 @@
         android:visibility="invisible" />
     <FrameLayout
         android:id="@+id/lockscreen_clock_view_large"
-        android:layout_marginTop="@dimen/keyguard_large_clock_top_margin"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:clipChildren="false"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
index e64b586..8497ff0 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
@@ -27,6 +27,7 @@
     android:layout_height="match_parent"
     android:clipChildren="false"
     android:clipToPadding="false"
+    android:paddingTop="@dimen/keyguard_lock_padding"
     android:importantForAccessibility="yes"> <!-- Needed because TYPE_WINDOW_STATE_CHANGED is sent
                                                   from this view when bouncer is shown -->
 
diff --git a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml b/packages/SystemUI/res/drawable/controls_panel_background.xml
similarity index 82%
rename from packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
rename to packages/SystemUI/res/drawable/controls_panel_background.xml
index 1992c77..9092877 100644
--- a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
+++ b/packages/SystemUI/res/drawable/controls_panel_background.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2022 The Android Open Source Project
   ~
@@ -12,9 +13,10 @@
   ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
+  ~
   -->
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="@color/dream_overlay_aqi_unknown" />
-    <corners android:radius="@dimen/dream_aqi_badge_corner_radius" />
+    <solid android:color="#1F1F1F" />
+    <corners android:radius="@dimen/notification_corner_radius" />
 </shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
index 41123c8..18fcebb 100644
--- a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
+++ b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
@@ -16,13 +16,53 @@
 * limitations under the License.
 */
 -->
-<shape
+<selector
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:shape="rectangle">
-  <solid android:color="?androidprv:attr/colorSurface"/>
-  <size
-      android:width="@dimen/keyguard_affordance_width"
-      android:height="@dimen/keyguard_affordance_height"/>
-  <corners android:radius="@dimen/keyguard_affordance_fixed_radius"/>
-</shape>
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+
+  <item android:state_selected="true">
+    <layer-list>
+      <item
+          android:left="3dp"
+          android:top="3dp"
+          android:right="3dp"
+          android:bottom="3dp">
+        <shape android:shape="oval">
+          <solid android:color="?androidprv:attr/colorSurface"/>
+          <size
+              android:width="@dimen/keyguard_affordance_width"
+              android:height="@dimen/keyguard_affordance_height"/>
+        </shape>
+      </item>
+
+      <item>
+        <shape android:shape="oval">
+          <stroke
+              android:color="@color/control_primary_text"
+              android:width="2dp"/>
+          <size
+              android:width="@dimen/keyguard_affordance_width"
+              android:height="@dimen/keyguard_affordance_height"/>
+        </shape>
+      </item>
+    </layer-list>
+  </item>
+
+  <item>
+    <layer-list>
+      <item
+          android:left="3dp"
+          android:top="3dp"
+          android:right="3dp"
+          android:bottom="3dp">
+        <shape android:shape="oval">
+          <solid android:color="?androidprv:attr/colorSurface"/>
+          <size
+              android:width="@dimen/keyguard_affordance_width"
+              android:height="@dimen/keyguard_affordance_height"/>
+        </shape>
+      </item>
+    </layer-list>
+  </item>
+
+</selector>
diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml
index 9efad22..ee3adba 100644
--- a/packages/SystemUI/res/layout/controls_with_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_with_favorites.xml
@@ -90,7 +90,7 @@
       android:layout_weight="1"
       android:layout_marginLeft="@dimen/global_actions_side_margin"
       android:layout_marginRight="@dimen/global_actions_side_margin"
-      android:background="#ff0000"
+      android:background="@drawable/controls_panel_background"
       android:padding="@dimen/global_actions_side_margin"
       android:visibility="gone"
       />
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_aqi.xml b/packages/SystemUI/res/layout/dream_overlay_complication_aqi.xml
deleted file mode 100644
index fcebb8d..0000000
--- a/packages/SystemUI/res/layout/dream_overlay_complication_aqi.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/aqi_view"
-    style="@style/clock_subtitle"
-    android:visibility="gone"
-    android:background="@drawable/dream_aqi_badge_bg"
-    android:paddingHorizontal="@dimen/dream_aqi_badge_padding_horizontal"
-    android:paddingVertical="@dimen/dream_aqi_badge_padding_vertical"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
deleted file mode 100644
index efbdd1a..0000000
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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.
--->
-<TextClock
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/date_view"
-    style="@style/clock_subtitle"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:format12Hour="@string/dream_date_complication_date_format"
-    android:format24Hour="@string/dream_date_complication_date_format"/>
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_weather.xml b/packages/SystemUI/res/layout/dream_overlay_complication_weather.xml
deleted file mode 100644
index f05922f..0000000
--- a/packages/SystemUI/res/layout/dream_overlay_complication_weather.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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.
--->
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/weather_view"
-    style="@style/clock_subtitle"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content" />
diff --git a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml b/packages/SystemUI/res/values-h411dp/dimens.xml
similarity index 74%
copy from packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
copy to packages/SystemUI/res/values-h411dp/dimens.xml
index 1992c77..6b21353 100644
--- a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
+++ b/packages/SystemUI/res/values-h411dp/dimens.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2022 The Android Open Source Project
   ~
@@ -13,8 +14,6 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="@color/dream_overlay_aqi_unknown" />
-    <corners android:radius="@dimen/dream_aqi_badge_corner_radius" />
-</shape>
\ No newline at end of file
+<resources>
+    <dimen name="volume_row_slider_height">137dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values-h700dp/dimens.xml b/packages/SystemUI/res/values-h700dp/dimens.xml
index 055308f..39777ab 100644
--- a/packages/SystemUI/res/values-h700dp/dimens.xml
+++ b/packages/SystemUI/res/values-h700dp/dimens.xml
@@ -17,4 +17,5 @@
 <resources>
     <!-- Margin above the ambient indication container -->
     <dimen name="ambient_indication_container_margin_top">15dp</dimen>
-</resources>
\ No newline at end of file
+    <dimen name="volume_row_slider_height">177dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml b/packages/SystemUI/res/values-h841dp/dimens.xml
similarity index 74%
copy from packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
copy to packages/SystemUI/res/values-h841dp/dimens.xml
index 1992c77..412da19 100644
--- a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
+++ b/packages/SystemUI/res/values-h841dp/dimens.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2022 The Android Open Source Project
   ~
@@ -13,8 +14,6 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="@color/dream_overlay_aqi_unknown" />
-    <corners android:radius="@dimen/dream_aqi_badge_corner_radius" />
-</shape>
\ No newline at end of file
+<resources>
+    <dimen name="volume_row_slider_height">237dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 70d53c7..ba6977a 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -246,15 +246,6 @@
 
     <color name="dream_overlay_camera_mic_off_dot_color">#FCBE03</color>
 
-    <!-- Air Quality -->
-    <color name="dream_overlay_aqi_good">#689F38</color>
-    <color name="dream_overlay_aqi_moderate">#FBC02D</color>
-    <color name="dream_overlay_aqi_unhealthy_sensitive">#F57C00</color>
-    <color name="dream_overlay_aqi_unhealthy">#C53929</color>
-    <color name="dream_overlay_aqi_very_unhealthy">#AD1457</color>
-    <color name="dream_overlay_aqi_hazardous">#880E4F</color>
-    <color name="dream_overlay_aqi_unknown">#BDC1C6</color>
-
     <!-- Dream overlay text shadows -->
     <color name="dream_overlay_clock_key_text_shadow_color">#4D000000</color>
     <color name="dream_overlay_clock_ambient_text_shadow_color">#4D000000</color>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 247e44d..6a87630 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -776,15 +776,11 @@
     <integer name="complicationFadeOutDelayMs">200</integer>
 
     <!-- Duration in milliseconds of the dream in un-blur animation. -->
-    <integer name="config_dreamOverlayInBlurDurationMs">249</integer>
-    <!-- Delay in milliseconds of the dream in un-blur animation. -->
-    <integer name="config_dreamOverlayInBlurDelayMs">133</integer>
+    <integer name="config_dreamOverlayInBlurDurationMs">250</integer>
     <!-- Duration in milliseconds of the dream in complications fade-in animation. -->
-    <integer name="config_dreamOverlayInComplicationsDurationMs">282</integer>
-    <!-- Delay in milliseconds of the dream in top complications fade-in animation. -->
-    <integer name="config_dreamOverlayInTopComplicationsDelayMs">216</integer>
-    <!-- Delay in milliseconds of the dream in bottom complications fade-in animation. -->
-    <integer name="config_dreamOverlayInBottomComplicationsDelayMs">299</integer>
+    <integer name="config_dreamOverlayInComplicationsDurationMs">250</integer>
+    <!-- Duration in milliseconds of the y-translation animation when entering a dream -->
+    <integer name="config_dreamOverlayInTranslationYDurationMs">917</integer>
 
     <!-- Icons that don't show in a collapsed non-keyguard statusbar -->
     <string-array name="config_collapsed_statusbar_icon_blocklist" translatable="false">
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 73baeac..227c0dd 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -758,6 +758,8 @@
     <dimen name="keyguard_affordance_fixed_height">48dp</dimen>
     <dimen name="keyguard_affordance_fixed_width">48dp</dimen>
     <dimen name="keyguard_affordance_fixed_radius">24dp</dimen>
+    <!-- Amount the button should shake when it's not long-pressed for long enough. -->
+    <dimen name="keyguard_affordance_shake_amplitude">8dp</dimen>
 
     <dimen name="keyguard_affordance_horizontal_offset">32dp</dimen>
     <dimen name="keyguard_affordance_vertical_offset">32dp</dimen>
@@ -1528,6 +1530,8 @@
     <dimen name="dream_overlay_status_bar_extra_margin">8dp</dimen>
 
     <!-- Dream overlay complications related dimensions -->
+    <!-- The blur radius applied to the dream overlay when entering and exiting dreams -->
+    <dimen name="dream_overlay_anim_blur_radius">50dp</dimen>
     <dimen name="dream_overlay_complication_clock_time_text_size">86dp</dimen>
     <dimen name="dream_overlay_complication_clock_time_translation_y">28dp</dimen>
     <dimen name="dream_overlay_complication_home_controls_padding">28dp</dimen>
@@ -1581,12 +1585,9 @@
     <dimen name="dream_overlay_complication_margin">0dp</dimen>
 
     <dimen name="dream_overlay_y_offset">80dp</dimen>
+    <dimen name="dream_overlay_entry_y_offset">40dp</dimen>
     <dimen name="dream_overlay_exit_y_offset">40dp</dimen>
 
-    <dimen name="dream_aqi_badge_corner_radius">28dp</dimen>
-    <dimen name="dream_aqi_badge_padding_vertical">6dp</dimen>
-    <dimen name="dream_aqi_badge_padding_horizontal">16dp</dimen>
-
     <dimen name="status_view_margin_horizontal">0dp</dimen>
 
     <!-- Media output broadcast dialog QR code picture size -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 5d4fa58..a2e03c3 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2792,9 +2792,6 @@
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is unknown -->
     <string name="bt_le_audio_broadcast_dialog_unknown_name">Unknown</string>
 
-    <!-- Date format for the Dream Date Complication [CHAR LIMIT=NONE] -->
-    <string name="dream_date_complication_date_format">EEE, MMM d</string>
-
     <!-- Time format for the Dream Time Complication for 12-hour time format [CHAR LIMIT=NONE] -->
     <string name="dream_time_complication_12_hr_time_format">h:mm</string>
 
@@ -2862,6 +2859,12 @@
     -->
     <string name="keyguard_affordance_enablement_dialog_home_instruction_2">&#8226; At least one device is available</string>
 
+    <!--
+    Error message shown when a button should be pressed and held to activate it, usually shown when
+    the user attempted to tap the button or held it for too short a time. [CHAR LIMIT=32].
+    -->
+    <string name="keyguard_affordance_press_too_short">Press and hold to activate</string>
+
     <!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] -->
     <string name="rear_display_bottom_sheet_cancel">Cancel</string>
     <!-- Text for the user to confirm they flipped the device around. [CHAR_LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
index c32de70..38c1640 100644
--- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml
+++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
@@ -124,6 +124,11 @@
         </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"/>
 
     <Include app:constraintSet="@xml/qs_header"/>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java
index e226d58..b057fe4 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java
@@ -362,8 +362,7 @@
         nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
         mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
         // TODO: Warn user.
-        Log.w(TAG, "Plugin has invalid interface version " + e.getActualVersion()
-                + ", expected " + e.getExpectedVersion());
+        Log.w(TAG, "Error loading plugin; " + e.getMessage());
     }
 
     /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
index 5883b6c..b927155 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -39,7 +39,6 @@
     private boolean mIsOrientationChanged;
     private SplitBounds mSplitBounds;
     private int mDesiredStagePosition;
-    private boolean mTaskbarInApp;
 
     public Matrix getMatrix() {
         return mMatrix;
@@ -58,10 +57,6 @@
         mDesiredStagePosition = desiredStagePosition;
     }
 
-    public void setTaskbarInApp(boolean taskbarInApp) {
-        mTaskbarInApp = taskbarInApp;
-    }
-
     /**
      * Updates the matrix based on the provided parameters
      */
@@ -79,34 +74,21 @@
         float scaledTaskbarSize;
         float canvasScreenRatio;
         if (mSplitBounds != null) {
-            float fullscreenTaskWidth;
-            float fullscreenTaskHeight;
-
-            float taskPercent;
             if (mSplitBounds.appsStackedVertically) {
-                taskPercent = mDesiredStagePosition != STAGE_POSITION_TOP_OR_LEFT
-                        ? mSplitBounds.topTaskPercent
-                        : (1 - (mSplitBounds.topTaskPercent + mSplitBounds.dividerHeightPercent));
-                // Scale portrait height to that of the actual screen
-                fullscreenTaskHeight = screenHeightPx * taskPercent;
-                if (mTaskbarInApp) {
-                    canvasScreenRatio = canvasHeight / fullscreenTaskHeight;
+                if (mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+                    // Top app isn't cropped at all by taskbar
+                    canvasScreenRatio = 0;
                 } else {
-                    if (mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
-                        // Top app isn't cropped at all by taskbar
-                        canvasScreenRatio = 0;
-                    } else {
-                        // Same as fullscreen ratio
-                        canvasScreenRatio = (float) canvasWidth / screenWidthPx;
-                    }
+                    // Same as fullscreen ratio
+                    canvasScreenRatio = (float) canvasWidth / screenWidthPx;
                 }
             } else {
                 // For landscape, scale the width
-                taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
+                float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
                         ? mSplitBounds.leftTaskPercent
                         : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent));
                 // Scale landscape width to that of actual screen
-                fullscreenTaskWidth = screenWidthPx * taskPercent;
+                float fullscreenTaskWidth = screenWidthPx * taskPercent;
                 canvasScreenRatio = canvasWidth / fullscreenTaskWidth;
             }
         } else {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index 860c8e3..7da27b1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -260,7 +260,8 @@
         if (reason != PROMPT_REASON_NONE) {
             int promtReasonStringRes = mView.getPromptReasonStringRes(reason);
             if (promtReasonStringRes != 0) {
-                mMessageAreaController.setMessage(promtReasonStringRes);
+                mMessageAreaController.setMessage(
+                        mView.getResources().getString(promtReasonStringRes), false);
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 40423cd..62babad 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -9,6 +9,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
@@ -43,6 +44,21 @@
     public static final int LARGE = 0;
     public static final int SMALL = 1;
 
+    /** Returns a region for the large clock to position itself, based on the given parent. */
+    public static Rect getLargeClockRegion(ViewGroup parent) {
+        int largeClockTopMargin = parent.getResources()
+                .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
+        int targetHeight = parent.getResources()
+                .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2;
+        int top = parent.getHeight() / 2 - targetHeight / 2
+                + largeClockTopMargin / 2;
+        return new Rect(
+                parent.getLeft(),
+                top,
+                parent.getRight(),
+                top + targetHeight);
+    }
+
     /**
      * Frame for small/large clocks
      */
@@ -129,17 +145,8 @@
             }
 
             if (mLargeClockFrame.isLaidOut()) {
-                int largeClockTopMargin = getResources()
-                        .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
-                int targetHeight = getResources()
-                        .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2;
-                int top = mLargeClockFrame.getHeight() / 2 - targetHeight / 2
-                        + largeClockTopMargin / 2;
-                mClock.getLargeClock().getEvents().onTargetRegionChanged(new Rect(
-                        mLargeClockFrame.getLeft(),
-                        top,
-                        mLargeClockFrame.getRight(),
-                        top + targetHeight));
+                mClock.getLargeClock().getEvents().onTargetRegionChanged(
+                        getLargeClockRegion(mLargeClockFrame));
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index e6aae9b..6ce84a9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -78,6 +78,7 @@
     private int mCurrentClockSize = SMALL;
 
     private int mKeyguardSmallClockTopMargin = 0;
+    private int mKeyguardLargeClockTopMargin = 0;
     private final ClockRegistry.ClockChangeListener mClockChangedListener;
 
     private ViewGroup mStatusArea;
@@ -164,6 +165,8 @@
         mClockEventController.registerListeners(mView);
         mKeyguardSmallClockTopMargin =
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
+        mKeyguardLargeClockTopMargin =
+                mView.getResources().getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
 
         if (mOnlyClock) {
             View ksv = mView.findViewById(R.id.keyguard_slice_view);
@@ -246,6 +249,8 @@
         mView.onDensityOrFontScaleChanged();
         mKeyguardSmallClockTopMargin =
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
+        mKeyguardLargeClockTopMargin =
+                mView.getResources().getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
         mView.updateClockTargetRegions();
     }
 
@@ -324,10 +329,18 @@
         }
 
         if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
+            // This gets the expected clock bottom if mLargeClockFrame had a top margin, but it's
+            // top margin only contributed to height and didn't move the top of the view (as this
+            // was the computation previously). As we no longer have a margin, we add this back
+            // into the computation manually.
             int frameHeight = mLargeClockFrame.getHeight();
             int clockHeight = clock.getLargeClock().getView().getHeight();
-            return frameHeight / 2 + clockHeight / 2;
+            return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2;
         } else {
+            // This is only called if we've never shown the large clock as the frame is inflated
+            // with 'gone', but then the visibility is never set when it is animated away by
+            // KeyguardClockSwitch, instead it is removed from the view hierarchy.
+            // TODO(b/261755021): Cleanup Large Frame Visibility
             int clockHeight = clock.getSmallClock().getView().getHeight();
             return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin;
         }
@@ -345,11 +358,15 @@
         if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
             return clock.getLargeClock().getView().getHeight();
         } else {
+            // Is not called except in certain edge cases, see comment in getClockBottom
+            // TODO(b/261755021): Cleanup Large Frame Visibility
             return clock.getSmallClock().getView().getHeight();
         }
     }
 
     boolean isClockTopAligned() {
+        // Returns false except certain edge cases, see comment in getClockBottom
+        // TODO(b/261755021): Cleanup Large Frame Visibility
         return mLargeClockFrame.getVisibility() != View.VISIBLE;
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 2e9ad58..d1c9a30 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -142,8 +142,11 @@
     }
 
     public void startAppearAnimation() {
-        if (TextUtils.isEmpty(mMessageAreaController.getMessage())) {
-            mMessageAreaController.setMessage(getInitialMessageResId());
+        if (TextUtils.isEmpty(mMessageAreaController.getMessage())
+                && getInitialMessageResId() != 0) {
+            mMessageAreaController.setMessage(
+                    mView.getResources().getString(getInitialMessageResId()),
+                    /* animate= */ false);
         }
         mView.startAppearAnimation();
     }
@@ -163,9 +166,7 @@
     }
 
     /** Determines the message to show in the bouncer when it first appears. */
-    protected int getInitialMessageResId() {
-        return 0;
-    }
+    protected abstract int getInitialMessageResId();
 
     /** Factory for a {@link KeyguardInputViewController}. */
     public static class Factory {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 5d86ccd..67e3400 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -52,6 +52,7 @@
     private int mYTransOffset;
     private View mBouncerMessageView;
     @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
+    public static final long ANIMATION_DURATION = 650;
 
     public KeyguardPINView(Context context) {
         this(context, null);
@@ -181,7 +182,7 @@
         if (mAppearAnimator.isRunning()) {
             mAppearAnimator.cancel();
         }
-        mAppearAnimator.setDuration(650);
+        mAppearAnimator.setDuration(ANIMATION_DURATION);
         mAppearAnimator.addUpdateListener(animation -> animate(animation.getAnimatedFraction()));
         mAppearAnimator.start();
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index c985fd7..c1fae9e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -24,6 +24,7 @@
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
+import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
 
 import android.animation.Animator;
@@ -107,6 +108,8 @@
                 return R.string.kg_prompt_reason_timeout_password;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 return R.string.kg_prompt_reason_timeout_password;
+            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
+                return R.string.kg_prompt_reason_timeout_password;
             case PROMPT_REASON_NONE:
                 return 0;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 571d274..0c17489 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -313,6 +313,9 @@
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern);
                 break;
+            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
+                mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern);
+                break;
             case PROMPT_REASON_NONE:
                 break;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
index c46e33d..0a91150 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -22,6 +22,7 @@
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
+import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
 
 import android.animation.Animator;
@@ -123,6 +124,8 @@
                 return R.string.kg_prompt_reason_timeout_pin;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 return R.string.kg_prompt_reason_timeout_pin;
+            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
+                return R.string.kg_prompt_reason_timeout_pin;
             case PROMPT_REASON_NONE:
                 return 0;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index f7423ed..8011efd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -139,4 +139,9 @@
         super.startErrorAnimation();
         mView.startErrorAnimation();
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_pin;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index f51ac32..35b2db2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -74,9 +74,4 @@
         return mView.startDisappearAnimation(
                 mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable);
     }
-
-    @Override
-    protected int getInitialMessageResId() {
-        return R.string.keyguard_enter_your_pin;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 8f3484a..5d7a6f1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -36,8 +36,11 @@
 
 import static java.lang.Integer.max;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.admin.DevicePolicyManager;
@@ -967,11 +970,23 @@
             }
 
             mUserSwitcherViewGroup.setAlpha(0f);
-            ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA,
-                    1f);
-            alphaAnim.setInterpolator(Interpolators.ALPHA_IN);
-            alphaAnim.setDuration(500);
-            alphaAnim.start();
+            ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+            int yTrans = mView.getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry);
+            animator.setInterpolator(Interpolators.STANDARD_DECELERATE);
+            animator.setDuration(650);
+            animator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mUserSwitcherViewGroup.setAlpha(1f);
+                    mUserSwitcherViewGroup.setTranslationY(0f);
+                }
+            });
+            animator.addUpdateListener(animation -> {
+                float value = (float) animation.getAnimatedValue();
+                mUserSwitcherViewGroup.setAlpha(value);
+                mUserSwitcherViewGroup.setTranslationY(yTrans - yTrans * value);
+            });
+            animator.start();
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
index ac00e94..67d77e5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
@@ -61,6 +61,12 @@
     int PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT = 7;
 
     /**
+     * Some auth is required because the trustagent expired either from timeout or manually by the
+     * user
+     */
+    int PROMPT_REASON_TRUSTAGENT_EXPIRED = 8;
+
+    /**
      * Reset the view and prepare to take input. This should do things like clearing the
      * password or pattern and clear error messages.
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
index a5c8c78..39b567f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
@@ -156,5 +156,10 @@
         @Override
         public void onStartingToHide() {
         }
+
+        @Override
+        protected int getInitialMessageResId() {
+            return 0;
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index f5aaecf..746616a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1651,7 +1651,7 @@
 
                 @Override
                 public void onAuthenticationFailed() {
-                    requestActiveUnlock(
+                    requestActiveUnlockDismissKeyguard(
                             ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
                             "fingerprintFailure");
                     handleFingerprintAuthFailed();
@@ -2583,6 +2583,18 @@
     }
 
     /**
+     * Attempts to trigger active unlock from trust agent with a request to dismiss the keyguard.
+     */
+    public void requestActiveUnlockDismissKeyguard(
+            @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            String extraReason
+    ) {
+        requestActiveUnlock(
+                requestOrigin,
+                extraReason + "-dismissKeyguard", true);
+    }
+
+    /**
      * Whether the UDFPS bouncer is showing.
      */
     public void setUdfpsBouncerShowing(boolean showing) {
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 632fcdc..0fc9ef9 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -22,6 +22,8 @@
 import android.os.HandlerThread;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.android.systemui.dagger.GlobalRootComponent;
 import com.android.systemui.dagger.SysUIComponent;
 import com.android.systemui.dagger.WMComponent;
@@ -53,6 +55,7 @@
         mContext = context;
     }
 
+    @Nullable
     protected abstract GlobalRootComponent.Builder getGlobalRootComponentBuilder();
 
     /**
@@ -69,6 +72,11 @@
      * Starts the initialization process. This stands up the Dagger graph.
      */
     public void init(boolean fromTest) throws ExecutionException, InterruptedException {
+        GlobalRootComponent.Builder globalBuilder = getGlobalRootComponentBuilder();
+        if (globalBuilder == null) {
+            return;
+        }
+
         mRootComponent = getGlobalRootComponentBuilder()
                 .context(mContext)
                 .instrumentationTest(fromTest)
@@ -119,6 +127,7 @@
                     .setBackAnimation(Optional.ofNullable(null))
                     .setDesktopMode(Optional.ofNullable(null));
         }
+
         mSysUIComponent = builder.build();
         if (initializeComponents) {
             mSysUIComponent.init();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
index 8aa3040..55c095b 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui
 
+import android.app.Application
 import android.content.Context
 import com.android.systemui.dagger.DaggerReferenceGlobalRootComponent
 import com.android.systemui.dagger.GlobalRootComponent
@@ -24,7 +25,17 @@
  * {@link SystemUIInitializer} that stands up AOSP SystemUI.
  */
 class SystemUIInitializerImpl(context: Context) : SystemUIInitializer(context) {
-    override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder {
-        return DaggerReferenceGlobalRootComponent.builder()
+
+    override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder? {
+        return when (Application.getProcessName()) {
+            SCREENSHOT_CROSS_PROFILE_PROCESS -> null
+            else -> DaggerReferenceGlobalRootComponent.builder()
+        }
+    }
+
+    companion object {
+        private const val SYSTEMUI_PROCESS = "com.android.systemui"
+        private const val SCREENSHOT_CROSS_PROFILE_PROCESS =
+                "$SYSTEMUI_PROCESS:screenshot_cross_profile"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING
deleted file mode 100644
index 794eba4..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-  "presubmit": [
-    {
-      // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+)
-      "name": "SystemUIGoogleBiometricsScreenshotTests",
-      "options": [
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
-    }
-  ]
-}
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
index e2ef247..58d40d3 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
@@ -28,7 +28,6 @@
 import android.os.UserHandle
 import android.util.Log
 import android.view.WindowManager
-import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.ActivityIntentHelper
 import com.android.systemui.dagger.qualifiers.Main
@@ -83,7 +82,7 @@
      */
     fun launchCamera(source: Int) {
         val intent: Intent = getStartCameraIntent()
-        intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source)
+        intent.putExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, source)
         val wouldLaunchResolverActivity = activityIntentHelper.wouldLaunchResolverActivity(
             intent, KeyguardUpdateMonitor.getCurrentUser()
         )
@@ -149,9 +148,4 @@
             cameraIntents.getInsecureCameraIntent()
         }
     }
-
-    companion object {
-        @VisibleForTesting
-        const val EXTRA_CAMERA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
index f8a2002..867faf9 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
@@ -29,6 +29,7 @@
                 MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE
         val DEFAULT_INSECURE_CAMERA_INTENT_ACTION =
                 MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA
+        const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"
 
         @JvmStatic
         fun getOverrideCameraPackage(context: Context): String? {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt
index 4aa597e..8d0edf8 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt
@@ -50,7 +50,12 @@
      * Setup an activity to handle enter/exit animations. [view] should be the root of the content.
      * Fade and translate together.
      */
-    fun observerForAnimations(view: ViewGroup, window: Window, intent: Intent): LifecycleObserver {
+    fun observerForAnimations(
+            view: ViewGroup,
+            window: Window,
+            intent: Intent,
+            animateY: Boolean = true
+    ): LifecycleObserver {
         return object : LifecycleObserver {
             var showAnimation = intent.getBooleanExtra(ControlsUiController.EXTRA_ANIMATE, false)
 
@@ -61,8 +66,12 @@
                 view.transitionAlpha = 0.0f
 
                 if (translationY == -1f) {
-                    translationY = view.context.resources.getDimensionPixelSize(
-                        R.dimen.global_actions_controls_y_translation).toFloat()
+                    if (animateY) {
+                        translationY = view.context.resources.getDimensionPixelSize(
+                                R.dimen.global_actions_controls_y_translation).toFloat()
+                    } else {
+                        translationY = 0f
+                    }
                 }
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
index 5d611c4..d8d8c0e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
@@ -70,7 +70,8 @@
             ControlsAnimations.observerForAnimations(
                 requireViewById<ViewGroup>(R.id.control_detail_root),
                 window,
-                intent
+                intent,
+                !featureFlags.isEnabled(Flags.USE_APP_PANELS)
             )
         )
 
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 fb678aa..1e3e5cd 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -186,7 +186,7 @@
         val allStructures = controlsController.get().getFavorites()
         val selected = getPreferredSelectedItem(allStructures)
         val anyPanels = controlsListingController.get().getCurrentServices()
-                .none { it.panelActivity != null }
+                .any { it.panelActivity != null }
 
         return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) {
             ControlsActivity::class.java
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
index 7143be2..f5764c2 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt
@@ -24,6 +24,10 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Color
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import com.android.systemui.R
 import com.android.systemui.util.boundsOnScreen
 import com.android.wm.shell.TaskView
 import java.util.concurrent.Executor
@@ -64,6 +68,16 @@
                 options.taskAlwaysOnTop = true
 
                 taskView.post {
+                    val roundedCorner =
+                        activityContext.resources.getDimensionPixelSize(
+                            R.dimen.notification_corner_radius
+                        )
+                    val radii = FloatArray(8) { roundedCorner.toFloat() }
+                    taskView.background =
+                        ShapeDrawable(RoundRectShape(radii, null, null)).apply {
+                            setTint(Color.TRANSPARENT)
+                        }
+                    taskView.clipToOutline = true
                     taskView.startActivity(
                         pendingIntent,
                         fillInIntent,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index 0087c84..9b8ef71 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -21,11 +21,12 @@
 import android.animation.ValueAnimator
 import android.view.View
 import android.view.animation.Interpolator
-import androidx.annotation.FloatRange
 import androidx.core.animation.doOnEnd
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dreams.complication.ComplicationHostViewController
 import com.android.systemui.dreams.complication.ComplicationLayoutParams
+import com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_BOTTOM
+import com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP
 import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position
 import com.android.systemui.dreams.dagger.DreamOverlayModule
 import com.android.systemui.statusbar.BlurUtils
@@ -41,16 +42,15 @@
     private val mComplicationHostViewController: ComplicationHostViewController,
     private val mStatusBarViewController: DreamOverlayStatusBarViewController,
     private val mOverlayStateController: DreamOverlayStateController,
+    @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int,
     @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION)
     private val mDreamInBlurAnimDurationMs: Long,
-    @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DELAY)
-    private val mDreamInBlurAnimDelayMs: Long,
     @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION)
     private val mDreamInComplicationsAnimDurationMs: Long,
-    @Named(DreamOverlayModule.DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY)
-    private val mDreamInTopComplicationsAnimDelayMs: Long,
-    @Named(DreamOverlayModule.DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY)
-    private val mDreamInBottomComplicationsAnimDelayMs: Long,
+    @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DISTANCE)
+    private val mDreamInTranslationYDistance: Int,
+    @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION)
+    private val mDreamInTranslationYDurationMs: Long,
     @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DISTANCE)
     private val mDreamOutTranslationYDistance: Int,
     @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DURATION)
@@ -74,7 +74,7 @@
      */
     private var mCurrentAlphaAtPosition = mutableMapOf<Int, Float>()
 
-    @FloatRange(from = 0.0, to = 1.0) private var mBlurProgress: Float = 0f
+    private var mCurrentBlurRadius: Float = 0f
 
     /** Starts the dream content and dream overlay entry animations. */
     @JvmOverloads
@@ -86,25 +86,23 @@
                 playTogether(
                     blurAnimator(
                         view = view,
-                        from = 1f,
-                        to = 0f,
+                        fromBlurRadius = mDreamBlurRadius.toFloat(),
+                        toBlurRadius = 0f,
                         durationMs = mDreamInBlurAnimDurationMs,
-                        delayMs = mDreamInBlurAnimDelayMs
+                        interpolator = Interpolators.EMPHASIZED_DECELERATE
                     ),
                     alphaAnimator(
                         from = 0f,
                         to = 1f,
                         durationMs = mDreamInComplicationsAnimDurationMs,
-                        delayMs = mDreamInTopComplicationsAnimDelayMs,
-                        position = ComplicationLayoutParams.POSITION_TOP
+                        interpolator = Interpolators.LINEAR
                     ),
-                    alphaAnimator(
-                        from = 0f,
-                        to = 1f,
-                        durationMs = mDreamInComplicationsAnimDurationMs,
-                        delayMs = mDreamInBottomComplicationsAnimDelayMs,
-                        position = ComplicationLayoutParams.POSITION_BOTTOM
-                    )
+                    translationYAnimator(
+                        from = mDreamInTranslationYDistance.toFloat(),
+                        to = 0f,
+                        durationMs = mDreamInTranslationYDurationMs,
+                        interpolator = Interpolators.EMPHASIZED_DECELERATE
+                    ),
                 )
                 doOnEnd {
                     mAnimator = null
@@ -130,47 +128,48 @@
                         view = view,
                         // Start the blurring wherever the entry animation ended, in
                         // case it was cancelled early.
-                        from = mBlurProgress,
-                        to = 1f,
-                        durationMs = mDreamOutBlurDurationMs
+                        fromBlurRadius = mCurrentBlurRadius,
+                        toBlurRadius = mDreamBlurRadius.toFloat(),
+                        durationMs = mDreamOutBlurDurationMs,
+                        interpolator = Interpolators.EMPHASIZED_ACCELERATE
                     ),
                     translationYAnimator(
                         from = 0f,
                         to = mDreamOutTranslationYDistance.toFloat(),
                         durationMs = mDreamOutTranslationYDurationMs,
                         delayMs = mDreamOutTranslationYDelayBottomMs,
-                        position = ComplicationLayoutParams.POSITION_BOTTOM,
-                        animInterpolator = Interpolators.EMPHASIZED_ACCELERATE
+                        positions = POSITION_BOTTOM,
+                        interpolator = Interpolators.EMPHASIZED_ACCELERATE
                     ),
                     translationYAnimator(
                         from = 0f,
                         to = mDreamOutTranslationYDistance.toFloat(),
                         durationMs = mDreamOutTranslationYDurationMs,
                         delayMs = mDreamOutTranslationYDelayTopMs,
-                        position = ComplicationLayoutParams.POSITION_TOP,
-                        animInterpolator = Interpolators.EMPHASIZED_ACCELERATE
+                        positions = POSITION_TOP,
+                        interpolator = Interpolators.EMPHASIZED_ACCELERATE
                     ),
                     alphaAnimator(
                         from =
                             mCurrentAlphaAtPosition.getOrDefault(
-                                key = ComplicationLayoutParams.POSITION_BOTTOM,
+                                key = POSITION_BOTTOM,
                                 defaultValue = 1f
                             ),
                         to = 0f,
                         durationMs = mDreamOutAlphaDurationMs,
                         delayMs = mDreamOutAlphaDelayBottomMs,
-                        position = ComplicationLayoutParams.POSITION_BOTTOM
+                        positions = POSITION_BOTTOM
                     ),
                     alphaAnimator(
                         from =
                             mCurrentAlphaAtPosition.getOrDefault(
-                                key = ComplicationLayoutParams.POSITION_TOP,
+                                key = POSITION_TOP,
                                 defaultValue = 1f
                             ),
                         to = 0f,
                         durationMs = mDreamOutAlphaDurationMs,
                         delayMs = mDreamOutAlphaDelayTopMs,
-                        position = ComplicationLayoutParams.POSITION_TOP
+                        positions = POSITION_TOP
                     )
                 )
                 doOnEnd {
@@ -194,20 +193,21 @@
 
     private fun blurAnimator(
         view: View,
-        from: Float,
-        to: Float,
+        fromBlurRadius: Float,
+        toBlurRadius: Float,
         durationMs: Long,
-        delayMs: Long = 0
+        delayMs: Long = 0,
+        interpolator: Interpolator = Interpolators.LINEAR
     ): Animator {
-        return ValueAnimator.ofFloat(from, to).apply {
+        return ValueAnimator.ofFloat(fromBlurRadius, toBlurRadius).apply {
             duration = durationMs
             startDelay = delayMs
-            interpolator = Interpolators.LINEAR
+            this.interpolator = interpolator
             addUpdateListener { animator: ValueAnimator ->
-                mBlurProgress = animator.animatedValue as Float
+                mCurrentBlurRadius = animator.animatedValue as Float
                 mBlurUtils.applyBlur(
                     viewRootImpl = view.viewRootImpl,
-                    radius = mBlurUtils.blurRadiusOfRatio(mBlurProgress).toInt(),
+                    radius = mCurrentBlurRadius.toInt(),
                     opaque = false
                 )
             }
@@ -218,18 +218,24 @@
         from: Float,
         to: Float,
         durationMs: Long,
-        delayMs: Long,
-        @Position position: Int
+        delayMs: Long = 0,
+        @Position positions: Int = POSITION_TOP or POSITION_BOTTOM,
+        interpolator: Interpolator = Interpolators.LINEAR
     ): Animator {
         return ValueAnimator.ofFloat(from, to).apply {
             duration = durationMs
             startDelay = delayMs
-            interpolator = Interpolators.LINEAR
+            this.interpolator = interpolator
             addUpdateListener { va: ValueAnimator ->
-                setElementsAlphaAtPosition(
-                    alpha = va.animatedValue as Float,
-                    position = position,
-                    fadingOut = to < from
+                ComplicationLayoutParams.iteratePositions(
+                    { position: Int ->
+                        setElementsAlphaAtPosition(
+                            alpha = va.animatedValue as Float,
+                            position = position,
+                            fadingOut = to < from
+                        )
+                    },
+                    positions
                 )
             }
         }
@@ -239,16 +245,21 @@
         from: Float,
         to: Float,
         durationMs: Long,
-        delayMs: Long,
-        @Position position: Int,
-        animInterpolator: Interpolator
+        delayMs: Long = 0,
+        @Position positions: Int = POSITION_TOP or POSITION_BOTTOM,
+        interpolator: Interpolator = Interpolators.LINEAR
     ): Animator {
         return ValueAnimator.ofFloat(from, to).apply {
             duration = durationMs
             startDelay = delayMs
-            interpolator = animInterpolator
+            this.interpolator = interpolator
             addUpdateListener { va: ValueAnimator ->
-                setElementsTranslationYAtPosition(va.animatedValue as Float, position)
+                ComplicationLayoutParams.iteratePositions(
+                    { position: Int ->
+                        setElementsTranslationYAtPosition(va.animatedValue as Float, position)
+                    },
+                    positions
+                )
             }
         }
     }
@@ -263,7 +274,7 @@
                 CrossFadeHelper.fadeIn(view, alpha, /* remap= */ false)
             }
         }
-        if (position == ComplicationLayoutParams.POSITION_TOP) {
+        if (position == POSITION_TOP) {
             mStatusBarViewController.setFadeAmount(alpha, fadingOut)
         }
     }
@@ -273,7 +284,7 @@
         mComplicationHostViewController.getViewsAtPosition(position).forEach { v ->
             v.translationY = translationY
         }
-        if (position == ComplicationLayoutParams.POSITION_TOP) {
+        if (position == POSITION_TOP) {
             mStatusBarViewController.setTranslationY(translationY)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.kt
new file mode 100644
index 0000000..8325356
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.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.systemui.dreams
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import javax.inject.Inject
+
+/**
+ * {@link DreamOverlayLifecycleOwner} is a concrete implementation of {@link LifecycleOwner}, which
+ * provides access to an associated {@link LifecycleRegistry}.
+ */
+class DreamOverlayLifecycleOwner @Inject constructor() : LifecycleOwner {
+    val registry: LifecycleRegistry = LifecycleRegistry(this)
+
+    override fun getLifecycle(): Lifecycle {
+        return registry
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
index d145f5c..87c5f51 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.dreams;
 
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_SERVICE_COMPONENT;
+
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -35,6 +37,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /**
  * {@link DreamOverlayRegistrant} is responsible for telling system server that SystemUI should be
@@ -98,12 +101,13 @@
     }
 
     @Inject
-    public DreamOverlayRegistrant(Context context, @Main Resources resources) {
+    public DreamOverlayRegistrant(Context context, @Main Resources resources,
+            @Named(DREAM_OVERLAY_SERVICE_COMPONENT) ComponentName dreamOverlayServiceComponent) {
         mContext = context;
         mResources = resources;
         mDreamManager = IDreamManager.Stub.asInterface(
                 ServiceManager.getService(DreamService.DREAM_SERVICE));
-        mOverlayServiceComponent = new ComponentName(mContext, DreamOverlayService.class);
+        mOverlayServiceComponent = dreamOverlayServiceComponent;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index b927ae7..16b4f99 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -30,6 +30,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleRegistry;
 import androidx.lifecycle.ViewModelStore;
 
@@ -44,7 +45,10 @@
 import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
+import com.android.systemui.touch.TouchInsetManager;
 
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
@@ -83,8 +87,12 @@
 
     private final ComplicationComponent mComplicationComponent;
 
+    private final com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent
+            mDreamComplicationComponent;
+
     private final DreamOverlayComponent mDreamOverlayComponent;
 
+    private final DreamOverlayLifecycleOwner mLifecycleOwner;
     private final LifecycleRegistry mLifecycleRegistry;
 
     private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor;
@@ -130,12 +138,16 @@
     public DreamOverlayService(
             Context context,
             @Main Executor executor,
+            DreamOverlayLifecycleOwner lifecycleOwner,
             WindowManager windowManager,
             ComplicationComponent.Factory complicationComponentFactory,
+            com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent.Factory
+                    dreamComplicationComponentFactory,
             DreamOverlayComponent.Factory dreamOverlayComponentFactory,
             DreamOverlayStateController stateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             UiEventLogger uiEventLogger,
+            TouchInsetManager touchInsetManager,
             @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
                     ComponentName lowLightDreamComponent) {
         mContext = context;
@@ -151,9 +163,16 @@
         final Complication.Host host =
                 () -> mExecutor.execute(DreamOverlayService.this::requestExit);
 
-        mComplicationComponent = complicationComponentFactory.create();
-        mDreamOverlayComponent = dreamOverlayComponentFactory.create(viewModelStore, host, null);
-        mLifecycleRegistry = mDreamOverlayComponent.getLifecycleRegistry();
+        mComplicationComponent = complicationComponentFactory.create(lifecycleOwner, host,
+                viewModelStore, touchInsetManager);
+        mDreamComplicationComponent = dreamComplicationComponentFactory.create(
+                mComplicationComponent.getVisibilityController(), touchInsetManager);
+        mDreamOverlayComponent = dreamOverlayComponentFactory.create(lifecycleOwner,
+                mComplicationComponent.getComplicationHostViewController(), touchInsetManager,
+                new HashSet<>(Arrays.asList(
+                        mDreamComplicationComponent.getHideComplicationTouchHandler())));
+        mLifecycleOwner = lifecycleOwner;
+        mLifecycleRegistry = mLifecycleOwner.getRegistry();
 
         mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED));
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index 5f942b6..ccfdd096 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.dreams;
 
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_ENABLED;
+
 import android.service.dreams.DreamService;
 import android.util.Log;
 
@@ -37,6 +39,7 @@
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /**
  * {@link DreamOverlayStateController} is the source of truth for Dream overlay configurations and
@@ -83,6 +86,7 @@
     }
 
     private final Executor mExecutor;
+    private final boolean mOverlayEnabled;
     private final ArrayList<Callback> mCallbacks = new ArrayList<>();
 
     @Complication.ComplicationType
@@ -94,14 +98,27 @@
 
     @VisibleForTesting
     @Inject
-    public DreamOverlayStateController(@Main Executor executor) {
+    public DreamOverlayStateController(@Main Executor executor,
+            @Named(DREAM_OVERLAY_ENABLED) boolean overlayEnabled) {
         mExecutor = executor;
+        mOverlayEnabled = overlayEnabled;
+        if (DEBUG) {
+            Log.d(TAG, "Dream overlay enabled:" + mOverlayEnabled);
+        }
     }
 
     /**
      * Adds a complication to be included on the dream overlay.
      */
     public void addComplication(Complication complication) {
+        if (!mOverlayEnabled) {
+            if (DEBUG) {
+                Log.d(TAG,
+                        "Ignoring adding complication due to overlay disabled:" + complication);
+            }
+            return;
+        }
+
         mExecutor.execute(() -> {
             if (mComplications.add(complication)) {
                 if (DEBUG) {
@@ -116,6 +133,14 @@
      * Removes a complication from inclusion on the dream overlay.
      */
     public void removeComplication(Complication complication) {
+        if (!mOverlayEnabled) {
+            if (DEBUG) {
+                Log.d(TAG,
+                        "Ignoring removing complication due to overlay disabled:" + complication);
+            }
+            return;
+        }
+
         mExecutor.execute(() -> {
             if (mComplications.remove(complication)) {
                 if (DEBUG) {
@@ -193,7 +218,7 @@
      * @return {@code true} if overlay is active, {@code false} otherwise.
      */
     public boolean isOverlayActive() {
-        return containsState(STATE_DREAM_OVERLAY_ACTIVE);
+        return mOverlayEnabled && containsState(STATE_DREAM_OVERLAY_ACTIVE);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
index 100ccc3..a2e11b2 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
@@ -138,19 +138,27 @@
                     final ComplicationId id = complication.getId();
                     final Complication.ViewHolder viewHolder = complication.getComplication()
                             .createView(complication);
+
+                    final View view = viewHolder.getView();
+
+                    if (view == null) {
+                        Log.e(TAG, "invalid complication view. null view supplied by ViewHolder");
+                        return;
+                    }
+
                     // Complications to be added before dream entry animations are finished are set
                     // to invisible and are animated in.
                     if (!mEntryAnimationsFinished) {
-                        viewHolder.getView().setVisibility(View.INVISIBLE);
+                        view.setVisibility(View.INVISIBLE);
                     }
                     mComplications.put(id, viewHolder);
-                    if (viewHolder.getView().getParent() != null) {
+                    if (view.getParent() != null) {
                         Log.e(TAG, "View for complication "
                                 + complication.getComplication().getClass()
                                 + " already has a parent. Make sure not to reuse complication "
                                 + "views!");
                     }
-                    mLayoutEngine.addComplication(id, viewHolder.getView(),
+                    mLayoutEngine.addComplication(id, view,
                             viewHolder.getLayoutParams(), viewHolder.getCategory());
                 });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
index 46ce7a9..3e9b010 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
@@ -30,7 +30,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position;
-import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.dreams.complication.dagger.ComplicationModule;
 import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.touch.TouchInsetManager;
 
@@ -50,7 +50,7 @@
  * their layout parameters and attributes. The management of this set is done by
  * {@link ComplicationHostViewController}.
  */
-@DreamOverlayComponent.DreamOverlayScope
+@ComplicationModule.ComplicationScope
 public class ComplicationLayoutEngine implements Complication.VisibilityController {
     public static final String TAG = "ComplicationLayoutEng";
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
index 1755cb92..99e19fc 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
@@ -251,9 +251,17 @@
      * position specified for this {@link ComplicationLayoutParams}.
      */
     public void iteratePositions(Consumer<Integer> consumer) {
+        iteratePositions(consumer, mPosition);
+    }
+
+    /**
+     * Iterates over the defined positions and invokes the specified {@link Consumer} for each
+     * position specified by the given {@code position}.
+     */
+    public static void iteratePositions(Consumer<Integer> consumer, @Position int position) {
         for (int currentPosition = FIRST_POSITION; currentPosition <= LAST_POSITION;
                 currentPosition <<= 1) {
-            if ((mPosition & currentPosition) == currentPosition) {
+            if ((position & currentPosition) == currentPosition) {
                 consumer.accept(currentPosition);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index ee00512..1065b94 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -136,8 +136,15 @@
             final boolean hasFavorites = mControlsComponent.getControlsController()
                     .map(c -> !c.getFavorites().isEmpty())
                     .orElse(false);
+            boolean hasPanels = false;
+            for (int i = 0; i < controlsServices.size(); i++) {
+                if (controlsServices.get(i).getPanelActivity() != null) {
+                    hasPanels = true;
+                    break;
+                }
+            }
             final ControlsComponent.Visibility visibility = mControlsComponent.getVisibility();
-            return hasFavorites && visibility != UNAVAILABLE;
+            return (hasFavorites || hasPanels) && visibility != UNAVAILABLE;
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationComponent.kt
index 8949709..8d133bd 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationComponent.kt
@@ -1,12 +1,29 @@
 package com.android.systemui.dreams.complication.dagger
 
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModelStore
+import com.android.systemui.dreams.complication.Complication
+import com.android.systemui.dreams.complication.ComplicationHostViewController
+import com.android.systemui.dreams.complication.ComplicationLayoutEngine
+import com.android.systemui.touch.TouchInsetManager
+import dagger.BindsInstance
 import dagger.Subcomponent
 
-@Subcomponent
+@Subcomponent(modules = [ComplicationModule::class])
+@ComplicationModule.ComplicationScope
 interface ComplicationComponent {
     /** Factory for generating [ComplicationComponent]. */
     @Subcomponent.Factory
     interface Factory {
-        fun create(): ComplicationComponent
+        fun create(
+            @BindsInstance lifecycleOwner: LifecycleOwner,
+            @BindsInstance host: Complication.Host,
+            @BindsInstance viewModelStore: ViewModelStore,
+            @BindsInstance touchInsetManager: TouchInsetManager
+        ): ComplicationComponent
     }
+
+    fun getComplicationHostViewController(): ComplicationHostViewController
+
+    fun getVisibilityController(): ComplicationLayoutEngine
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
index 09cc7c5..797906f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
@@ -24,13 +24,12 @@
 import com.android.internal.util.Preconditions;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dreams.dagger.DreamOverlayComponent;
-
-import javax.inject.Named;
 
 import dagger.Module;
 import dagger.Provides;
 
+import javax.inject.Named;
+
 /**
  * Module for providing a scoped host view.
  */
@@ -49,7 +48,7 @@
      */
     @Provides
     @Named(SCOPED_COMPLICATIONS_LAYOUT)
-    @DreamOverlayComponent.DreamOverlayScope
+    @ComplicationModule.ComplicationScope
     static ConstraintLayout providesComplicationHostView(
             LayoutInflater layoutInflater) {
         return Preconditions.checkNotNull((ConstraintLayout)
@@ -60,7 +59,6 @@
 
     @Provides
     @Named(COMPLICATION_MARGIN_DEFAULT)
-    @DreamOverlayComponent.DreamOverlayScope
     static int providesComplicationPadding(@Main Resources resources) {
         return resources.getDimensionPixelSize(R.dimen.dream_overlay_complication_margin);
     }
@@ -70,7 +68,6 @@
      */
     @Provides
     @Named(COMPLICATIONS_FADE_OUT_DURATION)
-    @DreamOverlayComponent.DreamOverlayScope
     static int providesComplicationsFadeOutDuration(@Main Resources resources) {
         return resources.getInteger(R.integer.complicationFadeOutMs);
     }
@@ -80,7 +77,6 @@
      */
     @Provides
     @Named(COMPLICATIONS_FADE_OUT_DELAY)
-    @DreamOverlayComponent.DreamOverlayScope
     static int providesComplicationsFadeOutDelay(@Main Resources resources) {
         return resources.getInteger(R.integer.complicationFadeOutDelayMs);
     }
@@ -90,7 +86,6 @@
      */
     @Provides
     @Named(COMPLICATIONS_FADE_IN_DURATION)
-    @DreamOverlayComponent.DreamOverlayScope
     static int providesComplicationsFadeInDuration(@Main Resources resources) {
         return resources.getInteger(R.integer.complicationFadeInMs);
     }
@@ -100,7 +95,6 @@
      */
     @Provides
     @Named(COMPLICATIONS_RESTORE_TIMEOUT)
-    @DreamOverlayComponent.DreamOverlayScope
     static int providesComplicationsRestoreTimeout(@Main Resources resources) {
         return resources.getInteger(R.integer.complicationRestoreMs);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java
index 5c2fdf5..dbf5ab0 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java
@@ -24,16 +24,16 @@
 import com.android.systemui.dreams.complication.Complication;
 import com.android.systemui.dreams.complication.ComplicationCollectionViewModel;
 import com.android.systemui.dreams.complication.ComplicationLayoutEngine;
+import com.android.systemui.touch.TouchInsetManager;
+
+import dagger.Module;
+import dagger.Provides;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
 
 import javax.inject.Named;
 import javax.inject.Scope;
-
-import dagger.Module;
-import dagger.Provides;
-
 /**
  * Module for housing components related to rendering complications.
  */
@@ -73,4 +73,13 @@
             ComplicationLayoutEngine engine) {
         return engine;
     }
+
+    /**
+     * Provides a new touch inset session instance for complication logic.
+     */
+    @Provides
+    static TouchInsetManager.TouchInsetSession providesTouchInsetSession(
+            TouchInsetManager manager) {
+        return manager.createSession();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 101f4a4..8770cd1 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -16,7 +16,9 @@
 
 package com.android.systemui.dreams.dagger;
 
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 
 import com.android.dream.lowlight.dagger.LowLightDreamModule;
@@ -24,14 +26,17 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
+import com.android.systemui.dreams.DreamOverlayService;
 import com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule;
+import com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent;
+
+import dagger.Module;
+import dagger.Provides;
 
 import java.util.Optional;
 
 import javax.inject.Named;
 
-import dagger.Module;
-import dagger.Provides;
 
 /**
  * Dagger Module providing Dream-related functionality.
@@ -41,14 +46,40 @@
             LowLightDreamModule.class,
         },
         subcomponents = {
+            ComplicationComponent.class,
             DreamOverlayComponent.class,
         })
 public interface DreamModule {
     String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user";
+    String DREAM_OVERLAY_SERVICE_COMPONENT = "dream_overlay_service_component";
+    String DREAM_OVERLAY_ENABLED = "dream_overlay_enabled";
 
     String DREAM_SUPPORTED = "dream_supported";
 
     /**
+     * Provides the dream component
+     */
+    @Provides
+    @Named(DREAM_OVERLAY_SERVICE_COMPONENT)
+    static ComponentName providesDreamOverlayService(Context context) {
+        return new ComponentName(context, DreamOverlayService.class);
+    }
+
+    /**
+     * Provides whether dream overlay is enabled.
+     */
+    @Provides
+    @Named(DREAM_OVERLAY_ENABLED)
+    static Boolean providesDreamOverlayEnabled(PackageManager packageManager,
+            @Named(DREAM_OVERLAY_SERVICE_COMPONENT) ComponentName component) {
+        try {
+            return packageManager.getServiceInfo(component, PackageManager.GET_META_DATA).enabled;
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    /**
      * Provides an instance of the dream backend.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
index fab4b86..0332f88 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
@@ -23,15 +23,13 @@
 import android.annotation.Nullable;
 
 import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LifecycleRegistry;
-import androidx.lifecycle.ViewModelStore;
 
 import com.android.systemui.dreams.DreamOverlayContainerViewController;
-import com.android.systemui.dreams.complication.Complication;
-import com.android.systemui.dreams.complication.dagger.ComplicationModule;
+import com.android.systemui.dreams.complication.ComplicationHostViewController;
 import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
 import com.android.systemui.dreams.touch.DreamTouchHandler;
 import com.android.systemui.dreams.touch.dagger.DreamTouchModule;
+import com.android.systemui.touch.TouchInsetManager;
 
 import dagger.BindsInstance;
 import dagger.Subcomponent;
@@ -49,15 +47,16 @@
 @Subcomponent(modules = {
         DreamTouchModule.class,
         DreamOverlayModule.class,
-        ComplicationModule.class,
 })
 @DreamOverlayComponent.DreamOverlayScope
 public interface DreamOverlayComponent {
     /** Simple factory for {@link DreamOverlayComponent}. */
     @Subcomponent.Factory
     interface Factory {
-        DreamOverlayComponent create(@BindsInstance ViewModelStore store,
-                @BindsInstance Complication.Host host,
+        DreamOverlayComponent create(
+                @BindsInstance LifecycleOwner lifecycleOwner,
+                @BindsInstance ComplicationHostViewController complicationHostViewController,
+                @BindsInstance TouchInsetManager touchInsetManager,
                 @BindsInstance @Named(DREAM_TOUCH_HANDLERS) @Nullable
                         Set<DreamTouchHandler> dreamTouchHandlers);
     }
@@ -71,12 +70,6 @@
     /** Builds a {@link DreamOverlayContainerViewController}. */
     DreamOverlayContainerViewController getDreamOverlayContainerViewController();
 
-    /** Builds a {@link androidx.lifecycle.LifecycleRegistry} */
-    LifecycleRegistry getLifecycleRegistry();
-
-    /** Builds a {@link androidx.lifecycle.LifecycleOwner} */
-    LifecycleOwner getLifecycleOwner();
-
     /** Builds a {@link DreamOverlayTouchMonitor} */
     DreamOverlayTouchMonitor getDreamOverlayTouchMonitor();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index fc448a3..4aa46d4 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -23,7 +23,6 @@
 
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LifecycleRegistry;
 
 import com.android.internal.util.Preconditions;
 import com.android.systemui.R;
@@ -33,14 +32,12 @@
 import com.android.systemui.dreams.touch.DreamTouchHandler;
 import com.android.systemui.touch.TouchInsetManager;
 
-import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.ElementsIntoSet;
 
 import java.util.HashSet;
 import java.util.Set;
-import java.util.concurrent.Executor;
 
 import javax.inject.Named;
 
@@ -53,14 +50,14 @@
     public static final String BURN_IN_PROTECTION_UPDATE_INTERVAL =
             "burn_in_protection_update_interval";
     public static final String MILLIS_UNTIL_FULL_JITTER = "millis_until_full_jitter";
+    public static final String DREAM_BLUR_RADIUS = "DREAM_BLUR_RADIUS";
     public static final String DREAM_IN_BLUR_ANIMATION_DURATION = "dream_in_blur_anim_duration";
-    public static final String DREAM_IN_BLUR_ANIMATION_DELAY = "dream_in_blur_anim_delay";
     public static final String DREAM_IN_COMPLICATIONS_ANIMATION_DURATION =
             "dream_in_complications_anim_duration";
-    public static final String DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY =
-            "dream_in_top_complications_anim_delay";
-    public static final String DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY =
-            "dream_in_bottom_complications_anim_delay";
+    public static final String DREAM_IN_TRANSLATION_Y_DISTANCE =
+            "dream_in_complications_translation_y";
+    public static final String DREAM_IN_TRANSLATION_Y_DURATION =
+            "dream_in_complications_translation_y_duration";
     public static final String DREAM_OUT_TRANSLATION_Y_DISTANCE =
             "dream_out_complications_translation_y";
     public static final String DREAM_OUT_TRANSLATION_Y_DURATION =
@@ -107,14 +104,6 @@
     /** */
     @Provides
     @DreamOverlayComponent.DreamOverlayScope
-    public static TouchInsetManager providesTouchInsetManager(@Main Executor executor,
-            DreamOverlayContainerView view) {
-        return new TouchInsetManager(executor, view);
-    }
-
-    /** */
-    @Provides
-    @DreamOverlayComponent.DreamOverlayScope
     public static DreamOverlayStatusBarView providesDreamOverlayStatusBarView(
             DreamOverlayContainerView view) {
         return Preconditions.checkNotNull(view.findViewById(R.id.dream_overlay_status_bar),
@@ -145,6 +134,15 @@
     }
 
     /**
+     * The blur radius applied to the dream overlay at dream entry and exit.
+     */
+    @Provides
+    @Named(DREAM_BLUR_RADIUS)
+    static int providesDreamBlurRadius(@Main Resources resources) {
+        return resources.getDimensionPixelSize(R.dimen.dream_overlay_anim_blur_radius);
+    }
+
+    /**
      * Duration in milliseconds of the dream in un-blur animation.
      */
     @Provides
@@ -154,15 +152,6 @@
     }
 
     /**
-     * Delay in milliseconds of the dream in un-blur animation.
-     */
-    @Provides
-    @Named(DREAM_IN_BLUR_ANIMATION_DELAY)
-    static long providesDreamInBlurAnimationDelay(@Main Resources resources) {
-        return (long) resources.getInteger(R.integer.config_dreamOverlayInBlurDelayMs);
-    }
-
-    /**
      * Duration in milliseconds of the dream in complications fade-in animation.
      */
     @Provides
@@ -172,22 +161,23 @@
     }
 
     /**
-     * Delay in milliseconds of the dream in top complications fade-in animation.
+     * Provides the number of pixels to translate complications when entering a dream.
      */
     @Provides
-    @Named(DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY)
-    static long providesDreamInTopComplicationsAnimationDelay(@Main Resources resources) {
-        return (long) resources.getInteger(R.integer.config_dreamOverlayInTopComplicationsDelayMs);
+    @Named(DREAM_IN_TRANSLATION_Y_DISTANCE)
+    @DreamOverlayComponent.DreamOverlayScope
+    static int providesDreamInComplicationsTranslationY(@Main Resources resources) {
+        return resources.getDimensionPixelSize(R.dimen.dream_overlay_entry_y_offset);
     }
 
     /**
-     * Delay in milliseconds of the dream in bottom complications fade-in animation.
+     * Provides the duration in ms of the y-translation when dream enters.
      */
     @Provides
-    @Named(DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY)
-    static long providesDreamInBottomComplicationsAnimationDelay(@Main Resources resources) {
-        return (long) resources.getInteger(
-                R.integer.config_dreamOverlayInBottomComplicationsDelayMs);
+    @Named(DREAM_IN_TRANSLATION_Y_DURATION)
+    @DreamOverlayComponent.DreamOverlayScope
+    static long providesDreamInComplicationsTranslationYDuration(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayInTranslationYDurationMs);
     }
 
     /**
@@ -252,18 +242,6 @@
 
     @Provides
     @DreamOverlayComponent.DreamOverlayScope
-    static LifecycleOwner providesLifecycleOwner(Lazy<LifecycleRegistry> lifecycleRegistryLazy) {
-        return () -> lifecycleRegistryLazy.get();
-    }
-
-    @Provides
-    @DreamOverlayComponent.DreamOverlayScope
-    static LifecycleRegistry providesLifecycleRegistry(LifecycleOwner lifecycleOwner) {
-        return new LifecycleRegistry(lifecycleOwner);
-    }
-
-    @Provides
-    @DreamOverlayComponent.DreamOverlayScope
     static Lifecycle providesLifecycle(LifecycleOwner lifecycleOwner) {
         return lifecycleOwner.getLifecycle();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandler.java
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java
rename to packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandler.java
index e276e0c..3a4578b 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandler.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.dreams.touch;
+package com.android.systemui.dreams.dreamcomplication;
 
-import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_OUT_DELAY;
-import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_RESTORE_TIMEOUT;
+import static com.android.systemui.dreams.dreamcomplication.dagger.ComplicationModule.COMPLICATIONS_FADE_OUT_DELAY;
+import static com.android.systemui.dreams.dreamcomplication.dagger.ComplicationModule.COMPLICATIONS_RESTORE_TIMEOUT;
 
 import android.util.Log;
 import android.view.MotionEvent;
@@ -28,6 +28,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.Complication;
+import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
+import com.android.systemui.dreams.touch.DreamTouchHandler;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationComponent.kt
new file mode 100644
index 0000000..f2fb48d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationComponent.kt
@@ -0,0 +1,21 @@
+package com.android.systemui.dreams.dreamcomplication.dagger
+
+import com.android.systemui.dreams.complication.Complication
+import com.android.systemui.dreams.dreamcomplication.HideComplicationTouchHandler
+import com.android.systemui.touch.TouchInsetManager
+import dagger.BindsInstance
+import dagger.Subcomponent
+
+@Subcomponent(modules = [ComplicationModule::class])
+interface ComplicationComponent {
+    /** Factory for generating [ComplicationComponent]. */
+    @Subcomponent.Factory
+    interface Factory {
+        fun create(
+            @BindsInstance visibilityController: Complication.VisibilityController,
+            @BindsInstance touchInsetManager: TouchInsetManager
+        ): ComplicationComponent
+    }
+
+    fun getHideComplicationTouchHandler(): HideComplicationTouchHandler
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationModule.kt b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationModule.kt
new file mode 100644
index 0000000..ef75ce1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationModule.kt
@@ -0,0 +1,28 @@
+package com.android.systemui.dreams.dreamcomplication.dagger
+
+import android.content.res.Resources
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Main
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module
+object ComplicationModule {
+    const val COMPLICATIONS_RESTORE_TIMEOUT = "complication_restore_timeout"
+    const val COMPLICATIONS_FADE_OUT_DELAY = "complication_fade_out_delay"
+
+    /** Provides the delay to wait for before fading out complications. */
+    @Provides
+    @Named(COMPLICATIONS_FADE_OUT_DELAY)
+    fun providesComplicationsFadeOutDelay(@Main resources: Resources): Int {
+        return resources.getInteger(R.integer.complicationFadeOutDelayMs)
+    }
+
+    /** Provides the timeout for restoring complication visibility. */
+    @Provides
+    @Named(COMPLICATIONS_RESTORE_TIMEOUT)
+    fun providesComplicationsRestoreTimeout(@Main resources: Resources): Int {
+        return resources.getInteger(R.integer.complicationRestoreMs)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java
index 7338ecb..dad0004 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java
@@ -23,7 +23,6 @@
  */
 @Module(includes = {
             BouncerSwipeModule.class,
-            HideComplicationModule.class,
         }, subcomponents = {
             InputSessionComponent.class,
 })
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/HideComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/HideComplicationModule.java
deleted file mode 100644
index 3800ff7..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/HideComplicationModule.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.touch.dagger;
-
-import com.android.systemui.dreams.touch.DreamTouchHandler;
-import com.android.systemui.dreams.touch.HideComplicationTouchHandler;
-
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.IntoSet;
-
-/**
- * Module for {@link HideComplicationTouchHandler}.
- */
-@Module
-public class HideComplicationModule {
-    /**
-     * Provides {@link HideComplicationTouchHandler} for inclusion in touch handling over the dream.
-     */
-    @Provides
-    @IntoSet
-    public static DreamTouchHandler providesHideComplicationTouchHandler(
-            HideComplicationTouchHandler touchHandler) {
-        return touchHandler;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 0f52a2b..1a244cc5 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -83,7 +83,7 @@
     val STABILITY_INDEX_FIX = releasedFlag(114, "stability_index_fix")
 
     // TODO(b/259559750): Tracking Bug
-    val SEMI_STABLE_SORT = unreleasedFlag(115, "semi_stable_sort", teamfood = true)
+    val SEMI_STABLE_SORT = releasedFlag(115, "semi_stable_sort")
 
     @JvmField
     val USE_ROUNDNESS_SOURCETYPES = unreleasedFlag(116, "use_roundness_sourcetype", teamfood = true)
@@ -290,7 +290,8 @@
     // TODO(b/254513168): Tracking Bug
     @JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple")
 
-    @JvmField val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media")
+    @JvmField
+    val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media", teamfood = true)
 
     // TODO(b/261734857): Tracking Bug
     @JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
@@ -405,9 +406,10 @@
     val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
 
     // 1600 - accessibility
+    // TODO(b/262224538): Tracking Bug
     @JvmField
     val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
-        unreleasedFlag(1600, "a11y_floating_menu_fling_spring_animations", teamfood = true)
+        releasedFlag(1600, "a11y_floating_menu_fling_spring_animations")
 
     // 1700 - clipboard
     @JvmField val CLIPBOARD_OVERLAY_REFACTOR = releasedFlag(1700, "clipboard_overlay_refactor")
@@ -440,6 +442,8 @@
     // 2300 - stylus
     @JvmField val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used")
     @JvmField val ENABLE_STYLUS_CHARGING_UI = unreleasedFlag(2301, "enable_stylus_charging_ui")
+    @JvmField
+    val ENABLE_USI_BATTERY_NOTIFICATIONS = unreleasedFlag(2302, "enable_usi_battery_notifications")
 
     // 2400 - performance tools and debugging info
     // TODO(b/238923086): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
index 4ae37c5..cbcede0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
@@ -21,14 +21,18 @@
 import android.content.ContentValues
 import android.content.Context
 import android.content.UriMatcher
+import android.content.pm.PackageManager
 import android.content.pm.ProviderInfo
 import android.database.Cursor
 import android.database.MatrixCursor
 import android.net.Uri
+import android.os.Binder
+import android.os.Bundle
 import android.util.Log
 import com.android.systemui.SystemUIAppComponentFactoryBase
 import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
 import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract
 import javax.inject.Inject
 import kotlinx.coroutines.runBlocking
@@ -37,6 +41,7 @@
     ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
 
     @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
+    @Inject lateinit var previewManager: KeyguardRemotePreviewManager
 
     private lateinit var contextAvailableCallback: ContextAvailableCallback
 
@@ -149,6 +154,21 @@
         return deleteSelection(uri, selectionArgs)
     }
 
+    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
+        return if (
+            requireContext()
+                .checkPermission(
+                    android.Manifest.permission.BIND_WALLPAPER,
+                    Binder.getCallingPid(),
+                    Binder.getCallingUid(),
+                ) == PackageManager.PERMISSION_GRANTED
+        ) {
+            previewManager.preview(extras)
+        } else {
+            null
+        }
+    }
+
     private fun insertSelection(values: ContentValues?): Uri? {
         if (values == null) {
             throw IllegalArgumentException("Cannot insert selection, no values passed in!")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 36c939d..9234136 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -25,6 +25,7 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_UNLOCK_ANIMATION;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
@@ -142,12 +143,12 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.concurrent.Executor;
 
-import dagger.Lazy;
-
 /**
  * Mediates requests related to the keyguard.  This includes queries about the
  * state of the keyguard, power management events that effect whether the keyguard
@@ -822,6 +823,9 @@
             } else if (trustAgentsEnabled
                     && (strongAuth & SOME_AUTH_REQUIRED_AFTER_USER_REQUEST) != 0) {
                 return KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
+            } else if (trustAgentsEnabled
+                    && (strongAuth & SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED) != 0) {
+                return KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
             } else if (any && ((strongAuth & STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) != 0
                     || mUpdateMonitor.isFingerprintLockedOut())) {
                 return KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT;
@@ -1614,7 +1618,7 @@
         // TODO: Rename all screen off/on references to interactive/sleeping
         synchronized (this) {
             mDeviceInteractive = true;
-            if (mPendingLock && !cameraGestureTriggered) {
+            if (mPendingLock && !cameraGestureTriggered && !mWakeAndUnlocking) {
                 doKeyguardLocked(null);
             }
             mAnimatingScreenOff = false;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 2558fab..394426d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -130,6 +130,7 @@
                             state(
                                 isFeatureEnabled = component.isEnabled(),
                                 hasFavorites = favorites?.isNotEmpty() == true,
+                                hasPanels = serviceInfos.any { it.panelActivity != null },
                                 hasServiceInfos = serviceInfos.isNotEmpty(),
                                 iconResourceId = component.getTileImageId(),
                                 visibility = component.getVisibility(),
@@ -148,13 +149,14 @@
     private fun state(
         isFeatureEnabled: Boolean,
         hasFavorites: Boolean,
+        hasPanels: Boolean,
         hasServiceInfos: Boolean,
         visibility: ControlsComponent.Visibility,
         @DrawableRes iconResourceId: Int?,
     ): KeyguardQuickAffordanceConfig.LockScreenState {
         return if (
             isFeatureEnabled &&
-                hasFavorites &&
+                (hasFavorites || hasPanels) &&
                 hasServiceInfos &&
                 iconResourceId != null &&
                 visibility == ControlsComponent.Visibility.AVAILABLE
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 748c6e8..57668c7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -34,7 +34,6 @@
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import dagger.Lazy
@@ -62,12 +61,20 @@
     private val isUsingRepository: Boolean
         get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
 
+    /**
+     * Whether the UI should use the long press gesture to activate quick affordances.
+     *
+     * If `false`, the UI goes back to using single taps.
+     */
+    val useLongPress: Boolean
+        get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
+
     /** Returns an observable for the quick affordance at the given position. */
     fun quickAffordance(
         position: KeyguardQuickAffordancePosition
     ): Flow<KeyguardQuickAffordanceModel> {
         return combine(
-            quickAffordanceInternal(position),
+            quickAffordanceAlwaysVisible(position),
             keyguardInteractor.isDozing,
             keyguardInteractor.isKeyguardShowing,
         ) { affordance, isDozing, isKeyguardShowing ->
@@ -80,6 +87,19 @@
     }
 
     /**
+     * Returns an observable for the quick affordance at the given position but always visible,
+     * regardless of lock screen state.
+     *
+     * This is useful for experiences like the lock screen preview mode, where the affordances must
+     * always be visible.
+     */
+    fun quickAffordanceAlwaysVisible(
+        position: KeyguardQuickAffordancePosition,
+    ): Flow<KeyguardQuickAffordanceModel> {
+        return quickAffordanceInternal(position)
+    }
+
+    /**
      * Notifies that a quick affordance has been "triggered" (clicked) by the user.
      *
      * @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of
@@ -290,15 +310,6 @@
         }
     }
 
-    private fun KeyguardQuickAffordancePosition.toSlotId(): String {
-        return when (this) {
-            KeyguardQuickAffordancePosition.BOTTOM_START ->
-                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
-            KeyguardQuickAffordancePosition.BOTTOM_END ->
-                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END
-        }
-    }
-
     private fun String.encode(slotId: String): String {
         return "$slotId$DELIMITER$this"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
index a18b036..2581b59 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
@@ -16,8 +16,17 @@
 
 package com.android.systemui.keyguard.shared.quickaffordance
 
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+
 /** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
 enum class KeyguardQuickAffordancePosition {
     BOTTOM_START,
-    BOTTOM_END,
+    BOTTOM_END;
+
+    fun toSlotId(): String {
+        return when (this) {
+            BOTTOM_START -> KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
+            BOTTOM_END -> KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index cbe512f..ae8edfe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -16,14 +16,19 @@
 
 package com.android.systemui.keyguard.ui.binder
 
+import android.annotation.SuppressLint
 import android.graphics.drawable.Animatable2
 import android.util.Size
 import android.util.TypedValue
+import android.view.MotionEvent
 import android.view.View
+import android.view.ViewConfiguration
 import android.view.ViewGroup
 import android.view.ViewPropertyAnimator
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.core.animation.CycleInterpolator
+import androidx.core.animation.ObjectAnimator
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
 import androidx.lifecycle.Lifecycle
@@ -38,8 +43,10 @@
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
@@ -52,6 +59,7 @@
  * view-binding, binding each view only once. It is okay and expected for the same instance of the
  * view-model to be reused for multiple view/view-binder bindings.
  */
+@OptIn(ExperimentalCoroutinesApi::class)
 object KeyguardBottomAreaViewBinder {
 
     private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
@@ -84,7 +92,8 @@
     fun bind(
         view: ViewGroup,
         viewModel: KeyguardBottomAreaViewModel,
-        falsingManager: FalsingManager,
+        falsingManager: FalsingManager?,
+        messageDisplayer: (Int) -> Unit,
     ): Binding {
         val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
         val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
@@ -108,6 +117,7 @@
                             view = startButton,
                             viewModel = buttonModel,
                             falsingManager = falsingManager,
+                            messageDisplayer = messageDisplayer,
                         )
                     }
                 }
@@ -118,6 +128,7 @@
                             view = endButton,
                             viewModel = buttonModel,
                             falsingManager = falsingManager,
+                            messageDisplayer = messageDisplayer,
                         )
                     }
                 }
@@ -222,10 +233,12 @@
         }
     }
 
+    @SuppressLint("ClickableViewAccessibility")
     private fun updateButton(
         view: ImageView,
         viewModel: KeyguardQuickAffordanceViewModel,
-        falsingManager: FalsingManager,
+        falsingManager: FalsingManager?,
+        messageDisplayer: (Int) -> Unit,
     ) {
         if (!viewModel.isVisible) {
             view.isVisible = false
@@ -281,21 +294,126 @@
                 },
             )
         )
+
         view.backgroundTintList =
-            Utils.getColorAttr(
-                view.context,
-                if (viewModel.isActivated) {
-                    com.android.internal.R.attr.colorAccentPrimary
-                } else {
-                    com.android.internal.R.attr.colorSurface
-                }
-            )
+            if (!viewModel.isSelected) {
+                Utils.getColorAttr(
+                    view.context,
+                    if (viewModel.isActivated) {
+                        com.android.internal.R.attr.colorAccentPrimary
+                    } else {
+                        com.android.internal.R.attr.colorSurface
+                    }
+                )
+            } else {
+                null
+            }
 
         view.isClickable = viewModel.isClickable
         if (viewModel.isClickable) {
-            view.setOnClickListener(OnClickListener(viewModel, falsingManager))
+            if (viewModel.useLongPress) {
+                view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer))
+            } else {
+                view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
+            }
         } else {
             view.setOnClickListener(null)
+            view.setOnTouchListener(null)
+        }
+
+        view.isSelected = viewModel.isSelected
+    }
+
+    private class OnTouchListener(
+        private val view: View,
+        private val viewModel: KeyguardQuickAffordanceViewModel,
+        private val messageDisplayer: (Int) -> Unit,
+    ) : View.OnTouchListener {
+
+        private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
+        private var longPressAnimator: ViewPropertyAnimator? = null
+        private var downTimestamp = 0L
+
+        @SuppressLint("ClickableViewAccessibility")
+        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
+            return when (event?.actionMasked) {
+                MotionEvent.ACTION_DOWN ->
+                    if (viewModel.configKey != null) {
+                        downTimestamp = System.currentTimeMillis()
+                        longPressAnimator =
+                            view
+                                .animate()
+                                .scaleX(PRESSED_SCALE)
+                                .scaleY(PRESSED_SCALE)
+                                .setDuration(longPressDurationMs)
+                                .withEndAction {
+                                    view.setOnClickListener {
+                                        viewModel.onClicked(
+                                            KeyguardQuickAffordanceViewModel.OnClickedParameters(
+                                                configKey = viewModel.configKey,
+                                                expandable = Expandable.fromView(view),
+                                            )
+                                        )
+                                    }
+                                    view.performClick()
+                                    view.setOnClickListener(null)
+                                }
+                        true
+                    } else {
+                        false
+                    }
+                MotionEvent.ACTION_MOVE -> {
+                    if (event.historySize > 0) {
+                        val distance =
+                            sqrt(
+                                (event.y - event.getHistoricalY(0)).pow(2) +
+                                    (event.x - event.getHistoricalX(0)).pow(2)
+                            )
+                        if (distance > ViewConfiguration.getTouchSlop()) {
+                            cancel()
+                        }
+                    }
+                    true
+                }
+                MotionEvent.ACTION_UP -> {
+                    if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
+                        messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
+                        val shakeAnimator =
+                            ObjectAnimator.ofFloat(
+                                view,
+                                "translationX",
+                                0f,
+                                view.context.resources
+                                    .getDimensionPixelSize(
+                                        R.dimen.keyguard_affordance_shake_amplitude
+                                    )
+                                    .toFloat(),
+                                0f,
+                            )
+                        shakeAnimator.duration = 300
+                        shakeAnimator.interpolator = CycleInterpolator(5f)
+                        shakeAnimator.start()
+                    }
+                    cancel()
+                    true
+                }
+                MotionEvent.ACTION_CANCEL -> {
+                    cancel()
+                    true
+                }
+                else -> false
+            }
+        }
+
+        private fun cancel() {
+            downTimestamp = 0L
+            longPressAnimator?.cancel()
+            longPressAnimator = null
+            view.animate().scaleX(1f).scaleY(1f)
+        }
+
+        companion object {
+            private const val PRESSED_SCALE = 1.5f
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
new file mode 100644
index 0000000..a5ae8ba5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.keyguard.ui.preview
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.hardware.display.DisplayManager
+import android.os.Bundle
+import android.os.IBinder
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.FrameLayout
+import com.android.keyguard.ClockEventController
+import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.R
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
+import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
+import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.runBlocking
+
+/** Renders the preview of the lock screen. */
+class KeyguardPreviewRenderer
+@AssistedInject
+constructor(
+    @Application private val context: Context,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    private val bottomAreaViewModel: KeyguardBottomAreaViewModel,
+    displayManager: DisplayManager,
+    private val windowManager: WindowManager,
+    private val clockController: ClockEventController,
+    private val clockRegistry: ClockRegistry,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    @Assisted bundle: Bundle,
+) {
+
+    val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
+    private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
+    private val height: Int = bundle.getInt(KEY_VIEW_HEIGHT)
+
+    private var host: SurfaceControlViewHost
+
+    val surfacePackage: SurfaceControlViewHost.SurfacePackage
+        get() = host.surfacePackage
+
+    private var clockView: View? = null
+
+    private val disposables = mutableSetOf<DisposableHandle>()
+    private var isDestroyed = false
+
+    init {
+        bottomAreaViewModel.enablePreviewMode(
+            initiallySelectedSlotId =
+                bundle.getString(
+                    KeyguardQuickAffordancePreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
+                ),
+        )
+        runBlocking(mainDispatcher) {
+            host =
+                SurfaceControlViewHost(
+                    context,
+                    displayManager.getDisplay(bundle.getInt(KEY_DISPLAY_ID)),
+                    hostToken,
+                )
+            disposables.add(DisposableHandle { host.release() })
+        }
+    }
+
+    fun render() {
+        runBlocking(mainDispatcher) {
+            val rootView = FrameLayout(context)
+
+            setUpBottomArea(rootView)
+            setUpClock(rootView)
+
+            rootView.measure(
+                View.MeasureSpec.makeMeasureSpec(
+                    windowManager.currentWindowMetrics.bounds.width(),
+                    View.MeasureSpec.EXACTLY
+                ),
+                View.MeasureSpec.makeMeasureSpec(
+                    windowManager.currentWindowMetrics.bounds.height(),
+                    View.MeasureSpec.EXACTLY
+                ),
+            )
+            rootView.layout(0, 0, rootView.measuredWidth, rootView.measuredHeight)
+
+            // This aspect scales the view to fit in the surface and centers it
+            val scale: Float =
+                (width / rootView.measuredWidth.toFloat()).coerceAtMost(
+                    height / rootView.measuredHeight.toFloat()
+                )
+
+            rootView.scaleX = scale
+            rootView.scaleY = scale
+            rootView.pivotX = 0f
+            rootView.pivotY = 0f
+            rootView.translationX = (width - scale * rootView.width) / 2
+            rootView.translationY = (height - scale * rootView.height) / 2
+
+            host.setView(rootView, rootView.measuredWidth, rootView.measuredHeight)
+        }
+    }
+
+    fun onSlotSelected(slotId: String) {
+        bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
+    }
+
+    fun destroy() {
+        isDestroyed = true
+        disposables.forEach { it.dispose() }
+    }
+
+    private fun setUpBottomArea(parentView: ViewGroup) {
+        val bottomAreaView =
+            LayoutInflater.from(context)
+                .inflate(
+                    R.layout.keyguard_bottom_area,
+                    parentView,
+                    false,
+                ) as KeyguardBottomAreaView
+        bottomAreaView.init(
+            viewModel = bottomAreaViewModel,
+        )
+        parentView.addView(
+            bottomAreaView,
+            FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.WRAP_CONTENT,
+                Gravity.BOTTOM,
+            ),
+        )
+    }
+
+    private fun setUpClock(parentView: ViewGroup) {
+        val clockChangeListener = ClockRegistry.ClockChangeListener { onClockChanged(parentView) }
+        clockRegistry.registerClockChangeListener(clockChangeListener)
+        disposables.add(
+            DisposableHandle { clockRegistry.unregisterClockChangeListener(clockChangeListener) }
+        )
+
+        clockController.registerListeners(parentView)
+        disposables.add(DisposableHandle { clockController.unregisterListeners() })
+
+        val receiver =
+            object : BroadcastReceiver() {
+                override fun onReceive(context: Context?, intent: Intent?) {
+                    clockController.clock?.events?.onTimeTick()
+                }
+            }
+        broadcastDispatcher.registerReceiver(
+            receiver,
+            IntentFilter().apply {
+                addAction(Intent.ACTION_TIME_TICK)
+                addAction(Intent.ACTION_TIME_CHANGED)
+            },
+        )
+        disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
+
+        onClockChanged(parentView)
+    }
+
+    private fun onClockChanged(parentView: ViewGroup) {
+        clockController.clock = clockRegistry.createCurrentClock()
+        clockController.clock
+            ?.largeClock
+            ?.events
+            ?.onTargetRegionChanged(KeyguardClockSwitch.getLargeClockRegion(parentView))
+        clockView?.let { parentView.removeView(it) }
+        clockView = clockController.clock?.largeClock?.view?.apply { parentView.addView(this) }
+    }
+
+    companion object {
+        private const val KEY_HOST_TOKEN = "host_token"
+        private const val KEY_VIEW_WIDTH = "width"
+        private const val KEY_VIEW_HEIGHT = "height"
+        private const val KEY_DISPLAY_ID = "display_id"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt
new file mode 100644
index 0000000..be1d3a1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.keyguard.ui.preview
+
+import android.os.Bundle
+import dagger.assisted.AssistedFactory
+
+@AssistedFactory
+interface KeyguardPreviewRendererFactory {
+    fun create(bundle: Bundle): KeyguardPreviewRenderer
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
new file mode 100644
index 0000000..50722d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.keyguard.ui.preview
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Message
+import android.os.Messenger
+import android.util.ArrayMap
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.runBlocking
+
+@SysUISingleton
+class KeyguardRemotePreviewManager
+@Inject
+constructor(
+    private val previewRendererFactory: KeyguardPreviewRendererFactory,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Background private val backgroundHandler: Handler,
+) {
+    private val activePreviews: ArrayMap<IBinder, PreviewLifecycleObserver> =
+        ArrayMap<IBinder, PreviewLifecycleObserver>()
+
+    fun preview(request: Bundle?): Bundle? {
+        if (request == null) {
+            return null
+        }
+
+        var observer: PreviewLifecycleObserver? = null
+        return try {
+            val renderer = previewRendererFactory.create(request)
+
+            // Destroy any previous renderer associated with this token.
+            activePreviews[renderer.hostToken]?.let { destroyObserver(it) }
+            observer = PreviewLifecycleObserver(renderer, mainDispatcher, ::destroyObserver)
+            activePreviews[renderer.hostToken] = observer
+            renderer.render()
+            renderer.hostToken?.linkToDeath(observer, 0)
+            val result = Bundle()
+            result.putParcelable(
+                KEY_PREVIEW_SURFACE_PACKAGE,
+                renderer.surfacePackage,
+            )
+            val messenger =
+                Messenger(
+                    Handler(
+                        backgroundHandler.looper,
+                        observer,
+                    )
+                )
+            val msg = Message.obtain()
+            msg.replyTo = messenger
+            result.putParcelable(KEY_PREVIEW_CALLBACK, msg)
+            result
+        } catch (e: Exception) {
+            Log.e(TAG, "Unable to generate preview", e)
+            observer?.let { destroyObserver(it) }
+            null
+        }
+    }
+
+    private fun destroyObserver(observer: PreviewLifecycleObserver) {
+        observer.onDestroy()?.let { hostToken ->
+            if (activePreviews[hostToken] === observer) {
+                activePreviews.remove(hostToken)
+            }
+        }
+    }
+
+    private class PreviewLifecycleObserver(
+        private val renderer: KeyguardPreviewRenderer,
+        private val mainDispatcher: CoroutineDispatcher,
+        private val requestDestruction: (PreviewLifecycleObserver) -> Unit,
+    ) : Handler.Callback, IBinder.DeathRecipient {
+
+        private var isDestroyed = false
+
+        override fun handleMessage(message: Message): Boolean {
+            when (message.what) {
+                KeyguardQuickAffordancePreviewConstants.MESSAGE_ID_SLOT_SELECTED -> {
+                    message.data
+                        .getString(
+                            KeyguardQuickAffordancePreviewConstants.KEY_SLOT_ID,
+                        )
+                        ?.let { slotId -> renderer.onSlotSelected(slotId = slotId) }
+                }
+                else -> requestDestruction(this)
+            }
+
+            return true
+        }
+
+        override fun binderDied() {
+            requestDestruction(this)
+        }
+
+        fun onDestroy(): IBinder? {
+            if (isDestroyed) {
+                return null
+            }
+
+            isDestroyed = true
+            val hostToken = renderer.hostToken
+            hostToken?.unlinkToDeath(this, 0)
+            runBlocking(mainDispatcher) { renderer.destroy() }
+            return hostToken
+        }
+    }
+
+    companion object {
+        private const val TAG = "KeyguardRemotePreviewManager"
+        @VisibleForTesting const val KEY_PREVIEW_SURFACE_PACKAGE = "surface_package"
+        @VisibleForTesting const val KEY_PREVIEW_CALLBACK = "callback"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 227796f..5d85680 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -24,13 +24,19 @@
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 /** View-model for the keyguard bottom area view */
+@OptIn(ExperimentalCoroutinesApi::class)
 class KeyguardBottomAreaViewModel
 @Inject
 constructor(
@@ -40,6 +46,20 @@
     private val burnInHelperWrapper: BurnInHelperWrapper,
 ) {
     /**
+     * Whether this view-model instance is powering the preview experience that renders exclusively
+     * in the wallpaper picker application. This should _always_ be `false` for the real lock screen
+     * experience.
+     */
+    private val isInPreviewMode = MutableStateFlow(false)
+
+    /**
+     * ID of the slot that's currently selected in the preview that renders exclusively in the
+     * wallpaper picker application. This is ignored for the actual, real lock screen experience.
+     */
+    private val selectedPreviewSlotId =
+        MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START)
+
+    /**
      * Whether quick affordances are "opaque enough" to be considered visible to and interactive by
      * the user. If they are not interactive, user input should not be allowed on them.
      *
@@ -66,7 +86,14 @@
     val isOverlayContainerVisible: Flow<Boolean> =
         keyguardInteractor.isDozing.map { !it }.distinctUntilChanged()
     /** An observable for the alpha level for the entire bottom area. */
-    val alpha: Flow<Float> = bottomAreaInteractor.alpha.distinctUntilChanged()
+    val alpha: Flow<Float> =
+        isInPreviewMode.flatMapLatest { isInPreviewMode ->
+            if (isInPreviewMode) {
+                flowOf(1f)
+            } else {
+                bottomAreaInteractor.alpha.distinctUntilChanged()
+            }
+        }
     /** An observable for whether the indication area should be padded. */
     val isIndicationAreaPadded: Flow<Boolean> =
         combine(startButton, endButton) { startButtonModel, endButtonModel ->
@@ -94,27 +121,61 @@
      * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
      */
     fun shouldConstrainToTopOfLockIcon(): Boolean =
-            bottomAreaInteractor.shouldConstrainToTopOfLockIcon()
+        bottomAreaInteractor.shouldConstrainToTopOfLockIcon()
+
+    /**
+     * Puts this view-model in "preview mode", which means it's being used for UI that is rendering
+     * the lock screen preview in wallpaper picker / settings and not the real experience on the
+     * lock screen.
+     *
+     * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one.
+     */
+    fun enablePreviewMode(initiallySelectedSlotId: String?) {
+        isInPreviewMode.value = true
+        onPreviewSlotSelected(
+            initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
+        )
+    }
+
+    /**
+     * Notifies that a slot with the given ID has been selected in the preview experience that is
+     * rendering in the wallpaper picker. This is ignored for the real lock screen experience.
+     *
+     * @see enablePreviewMode
+     */
+    fun onPreviewSlotSelected(slotId: String) {
+        selectedPreviewSlotId.value = slotId
+    }
 
     private fun button(
         position: KeyguardQuickAffordancePosition
     ): Flow<KeyguardQuickAffordanceViewModel> {
-        return combine(
-                quickAffordanceInteractor.quickAffordance(position),
-                bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
-                areQuickAffordancesFullyOpaque,
-            ) { model, animateReveal, isFullyOpaque ->
-                model.toViewModel(
-                    animateReveal = animateReveal,
-                    isClickable = isFullyOpaque,
-                )
-            }
-            .distinctUntilChanged()
+        return isInPreviewMode.flatMapLatest { isInPreviewMode ->
+            combine(
+                    if (isInPreviewMode) {
+                        quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
+                    } else {
+                        quickAffordanceInteractor.quickAffordance(position = position)
+                    },
+                    bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
+                    areQuickAffordancesFullyOpaque,
+                    selectedPreviewSlotId,
+                ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId ->
+                    model.toViewModel(
+                        animateReveal = !isInPreviewMode && animateReveal,
+                        isClickable = isFullyOpaque && !isInPreviewMode,
+                        isSelected =
+                            (isInPreviewMode && selectedPreviewSlotId == position.toSlotId()),
+                    )
+                }
+                .distinctUntilChanged()
+        }
     }
 
     private fun KeyguardQuickAffordanceModel.toViewModel(
         animateReveal: Boolean,
         isClickable: Boolean,
+        isSelected: Boolean,
     ): KeyguardQuickAffordanceViewModel {
         return when (this) {
             is KeyguardQuickAffordanceModel.Visible ->
@@ -131,6 +192,8 @@
                     },
                     isClickable = isClickable,
                     isActivated = activationState is ActivationState.Active,
+                    isSelected = isSelected,
+                    useLongPress = quickAffordanceInteractor.useLongPress,
                 )
             is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index 44f48f9..cf3a6da 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -29,6 +29,8 @@
     val onClicked: (OnClickedParameters) -> Unit = {},
     val isClickable: Boolean = false,
     val isActivated: Boolean = false,
+    val isSelected: Boolean = false,
+    val useLongPress: Boolean = false,
 ) {
     data class OnClickedParameters(
         val configKey: String,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index 3e5d337..bb833df 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -30,9 +30,11 @@
 import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
 import com.android.systemui.media.taptotransfer.MediaTttFlags;
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger;
+import com.android.systemui.media.taptotransfer.receiver.ChipReceiverInfo;
 import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger;
 import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger;
 import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo;
 
 import java.util.Optional;
 
@@ -95,19 +97,19 @@
     @Provides
     @SysUISingleton
     @MediaTttSenderLogger
-    static MediaTttLogger providesMediaTttSenderLogger(
+    static MediaTttLogger<ChipbarInfo> providesMediaTttSenderLogger(
             @MediaTttSenderLogBuffer LogBuffer buffer
     ) {
-        return new MediaTttLogger("Sender", buffer);
+        return new MediaTttLogger<>("Sender", buffer);
     }
 
     @Provides
     @SysUISingleton
     @MediaTttReceiverLogger
-    static MediaTttLogger providesMediaTttReceiverLogger(
+    static MediaTttLogger<ChipReceiverInfo> providesMediaTttReceiverLogger(
             @MediaTttReceiverLogBuffer LogBuffer buffer
     ) {
-        return new MediaTttLogger("Receiver", buffer);
+        return new MediaTttLogger<>("Receiver", buffer);
     }
 
     /** */
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 b55bedd..8aef938 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -18,17 +18,21 @@
 
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.temporarydisplay.TemporaryViewLogger
 
 /**
  * A logger for media tap-to-transfer events.
  *
  * @param deviceTypeTag the type of device triggering the logs -- "Sender" or "Receiver".
+ *
+ * TODO(b/245610654): We should de-couple the sender and receiver loggers, since they're vastly
+ * different experiences.
  */
-class MediaTttLogger(
+class MediaTttLogger<T : TemporaryViewInfo>(
     deviceTypeTag: String,
     buffer: LogBuffer
-) : TemporaryViewLogger(buffer, BASE_TAG + deviceTypeTag) {
+) : TemporaryViewLogger<T>(buffer, BASE_TAG + deviceTypeTag) {
     /** Logs a change in the chip state for the given [mediaRouteId]. */
     fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) {
         buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index 009595a..066c185 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.TintedIcon
+import com.android.systemui.temporarydisplay.TemporaryViewInfo
 
 /** Utility methods for media tap-to-transfer. */
 class MediaTttUtils {
@@ -47,7 +48,7 @@
         fun getIconInfoFromPackageName(
             context: Context,
             appPackageName: String?,
-            logger: MediaTttLogger
+            logger: MediaTttLogger<out TemporaryViewInfo>
         ): IconInfo {
             if (appPackageName != null) {
                 val packageManager = context.packageManager
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt
index 40ea1e6..11348ad 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt
@@ -35,6 +35,14 @@
     FAR_FROM_SENDER(
         StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
         MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_FAR_FROM_SENDER
+    ),
+    TRANSFER_TO_RECEIVER_SUCCEEDED(
+        StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+        MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_SUCCEEDED,
+    ),
+    TRANSFER_TO_RECEIVER_FAILED(
+        StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED,
+        MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_FAILED,
     );
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 1c3a53c..7b9d0b4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -45,8 +45,10 @@
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
 import com.android.systemui.temporarydisplay.TemporaryViewInfo
+import com.android.systemui.temporarydisplay.ViewPriority
 import com.android.systemui.util.animation.AnimationUtil.Companion.frames
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.android.systemui.util.wakelock.WakeLock
 import javax.inject.Inject
@@ -62,7 +64,7 @@
 open class MediaTttChipControllerReceiver @Inject constructor(
         private val commandQueue: CommandQueue,
         context: Context,
-        @MediaTttReceiverLogger logger: MediaTttLogger,
+        @MediaTttReceiverLogger logger: MediaTttLogger<ChipReceiverInfo>,
         windowManager: WindowManager,
         mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
@@ -73,7 +75,8 @@
         private val uiEventLogger: MediaTttReceiverUiEventLogger,
         private val viewUtil: ViewUtil,
         wakeLockBuilder: WakeLock.Builder,
-) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger>(
+        systemClock: SystemClock,
+) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger<ChipReceiverInfo>>(
         context,
         logger,
         windowManager,
@@ -83,6 +86,7 @@
         powerManager,
         R.layout.media_ttt_chip_receiver,
         wakeLockBuilder,
+        systemClock,
 ) {
     @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
     override val windowLayoutParams = commonWindowLayoutParams.apply {
@@ -123,8 +127,8 @@
         }
         uiEventLogger.logReceiverStateChange(chipState)
 
-        if (chipState == ChipStateReceiver.FAR_FROM_SENDER) {
-            removeView(routeInfo.id, removalReason = ChipStateReceiver.FAR_FROM_SENDER.name)
+        if (chipState != ChipStateReceiver.CLOSE_TO_SENDER) {
+            removeView(routeInfo.id, removalReason = chipState.name)
             return
         }
         if (appIcon == null) {
@@ -290,4 +294,5 @@
     override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER,
     override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER,
     override val id: String,
+    override val priority: ViewPriority = ViewPriority.NORMAL,
 ) : TemporaryViewInfo()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt
index 39a2763..6e515f2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt
@@ -34,7 +34,11 @@
     @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs")
     MEDIA_TTT_RECEIVER_CLOSE_TO_SENDER(982),
     @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs")
-    MEDIA_TTT_RECEIVER_FAR_FROM_SENDER(983);
+    MEDIA_TTT_RECEIVER_FAR_FROM_SENDER(983),
+    @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs")
+    MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_SUCCEEDED(1263),
+    @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs")
+    MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_FAILED(1264);
 
     override fun getId() = metricId
 }
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 ec1984d..9f44d98 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
@@ -30,6 +30,7 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.temporarydisplay.ViewPriority
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
 import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
@@ -46,7 +47,7 @@
     private val chipbarCoordinator: ChipbarCoordinator,
     private val commandQueue: CommandQueue,
     private val context: Context,
-    @MediaTttSenderLogger private val logger: MediaTttLogger,
+    @MediaTttSenderLogger private val logger: MediaTttLogger<ChipbarInfo>,
     private val mediaTttFlags: MediaTttFlags,
     private val uiEventLogger: MediaTttSenderUiEventLogger,
 ) : CoreStartable {
@@ -146,7 +147,7 @@
         routeInfo: MediaRoute2Info,
         undoCallback: IUndoMediaTransferCallback?,
         context: Context,
-        logger: MediaTttLogger,
+        logger: MediaTttLogger<ChipbarInfo>,
     ): ChipbarInfo {
         val packageName = routeInfo.clientPackageName
         val otherDeviceName = routeInfo.name.toString()
@@ -180,6 +181,7 @@
             wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
             timeoutMs = chipStateSender.timeout,
             id = routeInfo.id,
+            priority = ViewPriority.NORMAL,
         )
     }
 
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 f97385b..6c99b67 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -654,8 +654,9 @@
     }
 
     private void updateMLModelState() {
-        boolean newState = mIsEnabled && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false);
+        boolean newState =
+                mIsGesturalModeEnabled && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                        SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false);
 
         if (newState == mUseMLModel) {
             return;
@@ -785,7 +786,7 @@
             // ML model
             boolean withinMinRange = x < mMLEnableWidth + mLeftInset
                     || x >= (mDisplaySize.x - mMLEnableWidth - mRightInset);
-            if (!withinMinRange && mUseMLModel
+            if (!withinMinRange && mUseMLModel && !mMLModelIsLoading
                     && (results = getBackGesturePredictionsCategory(x, y, app)) != -1) {
                 withinRange = (results == 1);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 930de13..d1cf46c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -157,7 +157,7 @@
         private const val DEFAULT_TASK_MANAGER_ENABLED = true
         private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false
         private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true
-        private const val DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = false
+        private const val DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = true
     }
 
     override var newChangesSinceDialogWasDismissed = false
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index f92bbf7..8ceee1a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -741,6 +741,14 @@
         }
     };
 
+    /**
+     * Force all tiles to be redistributed across pages.
+     * Should be called when one of the following changes: rows, columns, number of tiles.
+     */
+    public void forceTilesRedistribution() {
+        mDistributeTiles = true;
+    }
+
     public interface PageListener {
         int INVALID_PAGE = -1;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 1827eaf..b2ca6b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -148,6 +148,11 @@
         }
     }
 
+    @Override
+    protected void onSplitShadeChanged() {
+        ((PagedTileLayout) mView.getOrCreateTileLayout()).forceTilesRedistribution();
+    }
+
     /** */
     public void setVisibility(int visibility) {
         mView.setVisibility(visibility);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index dd88c83..60d2c17 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -96,17 +96,23 @@
                         /* newOrientation= */ newConfig.orientation,
                         /* containerName= */ mView.getDumpableTag());
 
+                    boolean previousSplitShadeState = mShouldUseSplitNotificationShade;
                     mShouldUseSplitNotificationShade =
                         LargeScreenUtils.shouldUseSplitNotificationShade(getResources());
                     mLastOrientation = newConfig.orientation;
 
                     switchTileLayoutIfNeeded();
                     onConfigurationChanged();
+                    if (previousSplitShadeState != mShouldUseSplitNotificationShade) {
+                        onSplitShadeChanged();
+                    }
                 }
             };
 
     protected void onConfigurationChanged() { }
 
+    protected void onSplitShadeChanged() { }
+
     private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> {
         if (mMediaVisibilityChangedListener != null) {
             mMediaVisibilityChangedListener.accept(visible);
@@ -264,14 +270,6 @@
             }
         }
     }
-    protected QSTile getTile(String subPanel) {
-        for (int i = 0; i < mRecords.size(); i++) {
-            if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) {
-                return mRecords.get(i).tile;
-            }
-        }
-        return mHost.createTile(subPanel);
-    }
 
     boolean areThereTiles() {
         return !mRecords.isEmpty();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
index 9743c3e..206a620 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
@@ -358,6 +358,9 @@
                     if (!isChecked && shouldShowMobileDialog()) {
                         showTurnOffMobileDialog();
                     } else if (!shouldShowMobileDialog()) {
+                        if (mInternetDialogController.isMobileDataEnabled() == isChecked) {
+                            return;
+                        }
                         mInternetDialogController.setMobileDataEnabled(mContext, mDefaultDataSubId,
                                 isChecked, false);
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index b511b54..7fc0a5f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -101,6 +101,8 @@
         @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
@@ -429,8 +431,11 @@
         }
         header as MotionLayout
         if (largeScreenActive) {
-            header.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT).applyTo(header)
+            logInstantEvent("Large screen constraints set")
+            header.setTransition(HEADER_TRANSITION_ID)
+            header.transitionToStart()
         } else {
+            logInstantEvent("Small screen constraints set")
             header.setTransition(HEADER_TRANSITION_ID)
             header.transitionToStart()
             updatePosition()
@@ -440,15 +445,19 @@
 
     private fun updatePosition() {
         if (header is MotionLayout && !largeScreenActive && visible) {
-            Trace.instantForTrack(
-                TRACE_TAG_APP,
-                "LargeScreenHeaderController - updatePosition",
-                "position: $qsExpandedFraction"
-            )
+            logInstantEvent("updatePosition: $qsExpandedFraction")
             header.progress = qsExpandedFraction
         }
     }
 
+    private fun logInstantEvent(message: String) {
+        Trace.instantForTrack(
+                TRACE_TAG_APP,
+                "LargeScreenHeaderController",
+                message
+        )
+    }
+
     private fun updateListeners() {
         qsCarrierGroupController.setListening(visible)
         if (visible) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 568f6d3..61e53da 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -40,6 +40,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
 import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
 import static com.android.systemui.util.DumpUtilsKt.asIndenting;
@@ -1331,7 +1332,9 @@
         mKeyguardBottomArea.init(
                 mKeyguardBottomAreaViewModel,
                 mFalsingManager,
-                mLockIconViewController
+                mLockIconViewController,
+                stringResourceId ->
+                        mKeyguardIndicationController.showTransientIndication(stringResourceId)
         );
     }
 
@@ -2329,7 +2332,7 @@
 
 
     private boolean handleQsTouch(MotionEvent event) {
-        if (mSplitShadeEnabled && touchXOutsideOfQs(event.getX())) {
+        if (isSplitShadeAndTouchXOutsideQs(event.getX())) {
             return false;
         }
         final int action = event.getActionMasked();
@@ -2386,12 +2389,14 @@
         return false;
     }
 
-    private boolean touchXOutsideOfQs(float touchX) {
-        return touchX < mQsFrame.getX() || touchX > mQsFrame.getX() + mQsFrame.getWidth();
+    /** Returns whether split shade is enabled and an x coordinate is outside of the QS frame. */
+    private boolean isSplitShadeAndTouchXOutsideQs(float touchX) {
+        return mSplitShadeEnabled && (touchX < mQsFrame.getX()
+                || touchX > mQsFrame.getX() + mQsFrame.getWidth());
     }
 
     private boolean isInQsArea(float x, float y) {
-        if (touchXOutsideOfQs(x)) {
+        if (isSplitShadeAndTouchXOutsideQs(x)) {
             return false;
         }
         // Let's reject anything at the very bottom around the home handle in gesture nav
@@ -4724,6 +4729,7 @@
             if (!openingWithTouch || !mHasVibratedOnOpen) {
                 mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
                 mHasVibratedOnOpen = true;
+                mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true");
             }
         }
     }
@@ -5320,7 +5326,7 @@
         @Override
         public void flingTopOverscroll(float velocity, boolean open) {
             // in split shade mode we want to expand/collapse QS only when touch happens within QS
-            if (mSplitShadeEnabled && touchXOutsideOfQs(mInitialTouchX)) {
+            if (isSplitShadeAndTouchXOutsideQs(mInitialTouchX)) {
                 return;
             }
             mLastOverscroll = 0f;
@@ -5481,6 +5487,15 @@
             mBarState = statusBarState;
             mKeyguardShowing = keyguardShowing;
 
+            boolean fromShadeToKeyguard = statusBarState == KEYGUARD
+                    && (oldState == SHADE || oldState == SHADE_LOCKED);
+            if (mSplitShadeEnabled && fromShadeToKeyguard) {
+                // user can go to keyguard from different shade states and closing animation
+                // may not fully run - we always want to make sure we close QS when that happens
+                // as we never need QS open in fresh keyguard state
+                closeQs();
+            }
+
             if (oldState == KEYGUARD && (goingToFullShade
                     || statusBarState == StatusBarState.SHADE_LOCKED)) {
 
@@ -5500,27 +5515,12 @@
                 mKeyguardStatusBarViewController.animateKeyguardStatusBarIn();
 
                 mNotificationStackScrollLayoutController.resetScrollPosition();
-                // Only animate header if the header is visible. If not, it will partially
-                // animate out
-                // the top of QS
-                if (!mQsExpanded) {
-                    // TODO(b/185683835) Nicer clipping when using new spacial model
-                    if (mSplitShadeEnabled) {
-                        mQs.animateHeaderSlidingOut();
-                    }
-                }
             } else {
                 // this else branch means we are doing one of:
                 //  - from KEYGUARD to SHADE (but not fully expanded as when swiping from the top)
                 //  - from SHADE to KEYGUARD
                 //  - from SHADE_LOCKED to SHADE
                 //  - getting notified again about the current SHADE or KEYGUARD state
-                if (mSplitShadeEnabled && oldState == SHADE && statusBarState == KEYGUARD) {
-                    // user can go to keyguard from different shade states and closing animation
-                    // may not fully run - we always want to make sure we close QS when that happens
-                    // as we never need QS open in fresh keyguard state
-                    closeQs();
-                }
                 final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE
                         && statusBarState == KEYGUARD
                         && mScreenOffAnimationController.isKeyguardShowDelayed();
@@ -6124,6 +6124,7 @@
                     if (isFullyCollapsed()) {
                         // If panel is fully collapsed, reset haptic effect before adding movement.
                         mHasVibratedOnOpen = false;
+                        mShadeLog.logHasVibrated(mHasVibratedOnOpen, mExpandedFraction);
                     }
                     addMovement(event);
                     if (!isFullyCollapsed()) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 0b59af3..5fedbeb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -140,6 +140,15 @@
         })
     }
 
+    fun logHasVibrated(hasVibratedOnOpen: Boolean, fraction: Float) {
+        log(LogLevel.VERBOSE, {
+            bool1 = hasVibratedOnOpen
+            double1 = fraction.toDouble()
+        }, {
+            "hasVibratedOnOpen=$bool1, expansionFraction=$double1"
+        })
+    }
+
     fun logQsExpansionChanged(
             message: String,
             qsExpanded: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 39daa13..3072c81 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -33,6 +33,8 @@
     fun fullScreenIntentRequiresKeyguard(): Boolean =
         featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
 
+    fun fsiOnDNDUpdate(): Boolean = featureFlags.isEnabled(Flags.FSI_ON_DND_UPDATE)
+
     val isStabilityIndexFixEnabled: Boolean by lazy {
         featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 3e2dd05..aeae89c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -244,7 +244,7 @@
     }
 
     override fun onDozeAmountChanged(linear: Float, eased: Float) {
-        logger.logOnDozeAmountChanged(linear, eased)
+        logger.logOnDozeAmountChanged(linear = linear, eased = eased)
         if (overrideDozeAmountIfAnimatingScreenOff(linear)) {
             return
         }
@@ -263,6 +263,7 @@
 
     fun setDozeAmount(linear: Float, eased: Float, source: String) {
         val changed = linear != mLinearDozeAmount
+        logger.logSetDozeAmount(linear, eased, source, statusBarStateController.state, changed)
         mLinearDozeAmount = linear
         mDozeAmount = eased
         mDozeAmountSource = source
@@ -276,7 +277,7 @@
     }
 
     override fun onStateChanged(newState: Int) {
-        logger.logOnStateChanged(newState)
+        logger.logOnStateChanged(newState = newState, storedState = state)
         if (state == StatusBarState.SHADE && newState == StatusBarState.SHADE) {
             // The SHADE -> SHADE transition is only possible as part of cancelling the screen-off
             // animation (e.g. by fingerprint unlock).  This is done because the system is in an
@@ -324,12 +325,8 @@
     private fun overrideDozeAmountIfBypass(): Boolean {
         if (bypassController.bypassEnabled) {
             if (statusBarStateController.state == StatusBarState.KEYGUARD) {
-                logger.logSetDozeAmount("1.0", "1.0",
-                        "Override: bypass (keyguard)", StatusBarState.KEYGUARD)
                 setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)")
             } else {
-                logger.logSetDozeAmount("0.0", "0.0",
-                        "Override: bypass (shade)", statusBarStateController.state)
                 setDozeAmount(0f, 0f, source = "Override: bypass (shade)")
             }
             return true
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
index b40ce25..de18b0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
@@ -16,22 +16,33 @@
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.statusbar.StatusBarState
 import javax.inject.Inject
 
 class NotificationWakeUpCoordinatorLogger
 @Inject
 constructor(@NotificationLog private val buffer: LogBuffer) {
-    fun logSetDozeAmount(linear: String, eased: String, source: String, state: Int) {
+    fun logSetDozeAmount(
+        linear: Float,
+        eased: Float,
+        source: String,
+        state: Int,
+        changed: Boolean,
+    ) {
         buffer.log(
             TAG,
             DEBUG,
             {
-                str1 = linear
-                str2 = eased
+                double1 = linear.toDouble()
+                str2 = eased.toString()
                 str3 = source
                 int1 = state
+                bool1 = changed
             },
-            { "setDozeAmount: linear: $str1, eased: $str2, source: $str3, state: $int1" }
+            {
+                "setDozeAmount(linear=$double1, eased=$str2, source=$str3)" +
+                    " state=${StatusBarState.toString(int1)} changed=$bool1"
+            }
         )
     }
 
@@ -43,12 +54,23 @@
                 double1 = linear.toDouble()
                 str2 = eased.toString()
             },
-            { "onDozeAmountChanged($double1, $str2)" }
+            { "onDozeAmountChanged(linear=$double1, eased=$str2)" }
         )
     }
 
-    fun logOnStateChanged(newState: Int) {
-        buffer.log(TAG, DEBUG, { int1 = newState }, { "onStateChanged($int1)" })
+    fun logOnStateChanged(newState: Int, storedState: Int) {
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = newState
+                int2 = storedState
+            },
+            {
+                "onStateChanged(newState=${StatusBarState.toString(int1)})" +
+                    " stored=${StatusBarState.toString(int2)}"
+            }
+        )
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 5dbb4f9..1004ec1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -22,6 +22,7 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -38,6 +39,7 @@
 import com.android.systemui.statusbar.notification.dagger.IncomingHeader
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
 import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -70,11 +72,13 @@
     private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider,
     private val mRemoteInputManager: NotificationRemoteInputManager,
     private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
+    private val mFlags: NotifPipelineFlags,
     @IncomingHeader private val mIncomingHeaderController: NodeController,
     @Main private val mExecutor: DelayableExecutor,
 ) : Coordinator {
     private val mEntriesBindingUntil = ArrayMap<String, Long>()
     private val mEntriesUpdateTimes = ArrayMap<String, Long>()
+    private val mFSIUpdateCandidates = ArrayMap<String, Long>()
     private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
     private lateinit var mNotifPipeline: NotifPipeline
     private var mNow: Long = -1
@@ -278,7 +282,7 @@
         mPostedEntries.clear()
 
         // Also take this opportunity to clean up any stale entry update times
-        cleanUpEntryUpdateTimes()
+        cleanUpEntryTimes()
     }
 
     /**
@@ -384,8 +388,15 @@
         override fun onEntryAdded(entry: NotificationEntry) {
             // First check whether this notification should launch a full screen intent, and
             // launch it if needed.
-            if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) {
+            val fsiDecision = mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
+            if (fsiDecision != null && fsiDecision.shouldLaunch) {
+                mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
                 mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+            } else if (mFlags.fsiOnDNDUpdate() &&
+                fsiDecision.equals(FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)) {
+                // If DND was the only reason this entry was suppressed, note it for potential
+                // reconsideration on later ranking updates.
+                addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
             }
 
             // shouldHeadsUp includes check for whether this notification should be filtered
@@ -488,11 +499,32 @@
                 if (!isNewEnoughForRankingUpdate(entry)) continue
 
                 // The only entries we consider alerting for here are entries that have never
-                // interrupted and that now say they should heads up; if they've alerted in the
-                // past, we don't want to incorrectly alert a second time if there wasn't an
+                // interrupted and that now say they should heads up or FSI; if they've alerted in
+                // the past, we don't want to incorrectly alert a second time if there wasn't an
                 // explicit notification update.
                 if (entry.hasInterrupted()) continue
 
+                // Before potentially allowing heads-up, check for any candidates for a FSI launch.
+                // Any entry that is a candidate meets two criteria:
+                //   - was suppressed from FSI launch only by a DND suppression
+                //   - is within the recency window for reconsideration
+                // If any of these entries are no longer suppressed, launch the FSI now.
+                if (mFlags.fsiOnDNDUpdate() && isCandidateForFSIReconsideration(entry)) {
+                    val decision =
+                        mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
+                    if (decision.shouldLaunch) {
+                        // Log both the launch of the full screen and also that this was via a
+                        // ranking update.
+                        mLogger.logEntryUpdatedToFullScreen(entry.key)
+                        mNotificationInterruptStateProvider.logFullScreenIntentDecision(
+                            entry, decision)
+                        mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+
+                        // if we launch the FSI then this is no longer a candidate for HUN
+                        continue
+                    }
+                }
+
                 // The cases where we should consider this notification to be updated:
                 // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
                 //   state
@@ -528,6 +560,15 @@
     }
 
     /**
+     * Add the entry to the list of entries potentially considerable for FSI ranking update, where
+     * the provided time is the time the entry was added.
+     */
+    @VisibleForTesting
+    fun addForFSIReconsideration(entry: NotificationEntry, time: Long) {
+        mFSIUpdateCandidates[entry.key] = time
+    }
+
+    /**
      * Checks whether the entry is new enough to be updated via ranking update.
      * We want to avoid updating an entry too long after it was originally posted/updated when we're
      * only reacting to a ranking change, as relevant ranking updates are expected to come in
@@ -541,17 +582,38 @@
         return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS
     }
 
-    private fun cleanUpEntryUpdateTimes() {
+    /**
+     * Checks whether the entry is present new enough for reconsideration for full screen launch.
+     * The time window is the same as for ranking update, but this doesn't allow a potential update
+     * to an entry with full screen intent to count for timing purposes.
+     */
+    private fun isCandidateForFSIReconsideration(entry: NotificationEntry): Boolean {
+        val addedTime = mFSIUpdateCandidates[entry.key] ?: return false
+        return (mSystemClock.currentTimeMillis() - addedTime) <= MAX_RANKING_UPDATE_DELAY_MS
+    }
+
+    private fun cleanUpEntryTimes() {
         // Because we won't update entries that are older than this amount of time anyway, clean
-        // up any entries that are too old to notify.
+        // up any entries that are too old to notify from both the general and FSI specific lists.
+
+        // Anything newer than this time is still within the window.
+        val timeThreshold = mSystemClock.currentTimeMillis() - MAX_RANKING_UPDATE_DELAY_MS
+
         val toRemove = ArraySet<String>()
         for ((key, updateTime) in mEntriesUpdateTimes) {
-            if (updateTime == null ||
-                    (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) {
+            if (updateTime == null || timeThreshold > updateTime) {
                 toRemove.add(key)
             }
         }
         mEntriesUpdateTimes.removeAll(toRemove)
+
+        val toRemoveForFSI = ArraySet<String>()
+        for ((key, addedTime) in mFSIUpdateCandidates) {
+            if (addedTime == null || timeThreshold > addedTime) {
+                toRemoveForFSI.add(key)
+            }
+        }
+        mFSIUpdateCandidates.removeAll(toRemoveForFSI)
     }
 
     /** When an action is pressed on a notification, end HeadsUp lifetime extension. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 473c35d..2c6bf6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -70,6 +70,14 @@
         })
     }
 
+    fun logEntryUpdatedToFullScreen(key: String) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = key
+        }, {
+            "updating entry to launch full screen intent: $str1"
+        })
+    }
+
     fun logSummaryMarkedInterrupted(summaryKey: String, childKey: String) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = summaryKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
index 0380fff..1fcf17f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
@@ -17,10 +17,14 @@
 
 package com.android.systemui.statusbar.notification.logging
 
+import android.app.Notification
+
 /** Describes usage of a notification. */
 data class NotificationMemoryUsage(
     val packageName: String,
+    val uid: Int,
     val notificationKey: String,
+    val notification: Notification,
     val objectUsage: NotificationObjectUsage,
     val viewUsage: List<NotificationViewUsage>
 )
@@ -34,7 +38,8 @@
     val smallIcon: Int,
     val largeIcon: Int,
     val extras: Int,
-    val style: String?,
+    /** Style type, integer from [android.stats.sysui.NotificationEnums] */
+    val style: Int,
     val styleIcon: Int,
     val bigPicture: Int,
     val extender: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt
new file mode 100644
index 0000000..ffd931c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt
@@ -0,0 +1,173 @@
+/*
+ *
+ * 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.notification.logging
+
+import android.stats.sysui.NotificationEnums
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/** Dumps current notification memory use to bug reports for easier debugging. */
+@SysUISingleton
+class NotificationMemoryDumper
+@Inject
+constructor(val dumpManager: DumpManager, val notificationPipeline: NotifPipeline) : Dumpable {
+
+    fun init() {
+        dumpManager.registerNormalDumpable(javaClass.simpleName, this)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs)
+                .sortedWith(compareBy({ it.packageName }, { it.notificationKey }))
+        dumpNotificationObjects(pw, memoryUse)
+        dumpNotificationViewUsage(pw, memoryUse)
+    }
+
+    /** Renders a table of notification object usage into passed [PrintWriter]. */
+    private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) {
+        pw.println("Notification Object Usage")
+        pw.println("-----------")
+        pw.println(
+            "Package".padEnd(35) +
+                "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom"
+        )
+        pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView")
+        pw.println()
+
+        memoryUse.forEach { use ->
+            pw.println(
+                use.packageName.padEnd(35) +
+                    "\t\t" +
+                    "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" +
+                    (styleEnumToString(use.objectUsage.style).take(15) ?: "").padEnd(15) +
+                    "\t\t${use.objectUsage.styleIcon}\t" +
+                    "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" +
+                    "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" +
+                    use.notificationKey
+            )
+        }
+
+        // Calculate totals for easily glanceable summary.
+        data class Totals(
+            var smallIcon: Int = 0,
+            var largeIcon: Int = 0,
+            var styleIcon: Int = 0,
+            var bigPicture: Int = 0,
+            var extender: Int = 0,
+            var extras: Int = 0,
+        )
+
+        val totals =
+            memoryUse.fold(Totals()) { t, usage ->
+                t.smallIcon += usage.objectUsage.smallIcon
+                t.largeIcon += usage.objectUsage.largeIcon
+                t.styleIcon += usage.objectUsage.styleIcon
+                t.bigPicture += usage.objectUsage.bigPicture
+                t.extender += usage.objectUsage.extender
+                t.extras += usage.objectUsage.extras
+                t
+            }
+
+        pw.println()
+        pw.println("TOTALS")
+        pw.println(
+            "".padEnd(35) +
+                "\t\t" +
+                "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" +
+                "".padEnd(15) +
+                "\t\t${toKb(totals.styleIcon)}\t" +
+                "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" +
+                toKb(totals.extras)
+        )
+        pw.println()
+    }
+
+    /** Renders a table of notification view usage into passed [PrintWriter] */
+    private fun dumpNotificationViewUsage(
+        pw: PrintWriter,
+        memoryUse: List<NotificationMemoryUsage>,
+    ) {
+
+        data class Totals(
+            var smallIcon: Int = 0,
+            var largeIcon: Int = 0,
+            var style: Int = 0,
+            var customViews: Int = 0,
+            var softwareBitmapsPenalty: Int = 0,
+        )
+
+        val totals = Totals()
+        pw.println("Notification View Usage")
+        pw.println("-----------")
+        pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware")
+        pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps")
+        pw.println()
+        memoryUse
+            .filter { it.viewUsage.isNotEmpty() }
+            .forEach { use ->
+                pw.println(use.packageName + " " + use.notificationKey)
+                use.viewUsage.forEach { view ->
+                    pw.println(
+                        "  ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" +
+                            "\t${view.largeIcon}\t${view.style}" +
+                            "\t${view.customViews}\t${view.softwareBitmapsPenalty}"
+                    )
+
+                    if (view.viewType == ViewType.TOTAL) {
+                        totals.smallIcon += view.smallIcon
+                        totals.largeIcon += view.largeIcon
+                        totals.style += view.style
+                        totals.customViews += view.customViews
+                        totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty
+                    }
+                }
+            }
+        pw.println()
+        pw.println("TOTALS")
+        pw.println(
+            "  ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" +
+                "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" +
+                "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}"
+        )
+        pw.println()
+    }
+
+    private fun styleEnumToString(styleEnum: Int): String =
+        when (styleEnum) {
+            NotificationEnums.STYLE_UNSPECIFIED -> "Unspecified"
+            NotificationEnums.STYLE_NONE -> "None"
+            NotificationEnums.STYLE_BIG_PICTURE -> "BigPicture"
+            NotificationEnums.STYLE_BIG_TEXT -> "BigText"
+            NotificationEnums.STYLE_CALL -> "Call"
+            NotificationEnums.STYLE_DECORATED_CUSTOM_VIEW -> "DCustomView"
+            NotificationEnums.STYLE_INBOX -> "Inbox"
+            NotificationEnums.STYLE_MEDIA -> "Media"
+            NotificationEnums.STYLE_MESSAGING -> "Messaging"
+            NotificationEnums.STYLE_RANKER_GROUP -> "RankerGroup"
+            else -> "Unknown"
+        }
+
+    private fun toKb(bytes: Int): String {
+        return (bytes / 1024).toString() + " KB"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
new file mode 100644
index 0000000..ec8501a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
@@ -0,0 +1,194 @@
+/*
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.StatsManager
+import android.util.StatsEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.util.traceSection
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.runBlocking
+
+/** Periodically logs current state of notification memory consumption. */
+@SysUISingleton
+class NotificationMemoryLogger
+@Inject
+constructor(
+    private val notificationPipeline: NotifPipeline,
+    private val statsManager: StatsManager,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Background private val backgroundExecutor: Executor
+) : StatsManager.StatsPullAtomCallback {
+
+    /**
+     * This class is used to accumulate and aggregate data - the fields mirror values in statd Atom
+     * with ONE IMPORTANT difference - the values are in bytes, not KB!
+     */
+    internal data class NotificationMemoryUseAtomBuilder(val uid: Int, val style: Int) {
+        var count: Int = 0
+        var countWithInflatedViews: Int = 0
+        var smallIconObject: Int = 0
+        var smallIconBitmapCount: Int = 0
+        var largeIconObject: Int = 0
+        var largeIconBitmapCount: Int = 0
+        var bigPictureObject: Int = 0
+        var bigPictureBitmapCount: Int = 0
+        var extras: Int = 0
+        var extenders: Int = 0
+        var smallIconViews: Int = 0
+        var largeIconViews: Int = 0
+        var systemIconViews: Int = 0
+        var styleViews: Int = 0
+        var customViews: Int = 0
+        var softwareBitmaps: Int = 0
+        var seenCount = 0
+    }
+
+    fun init() {
+        statsManager.setPullAtomCallback(
+            SysUiStatsLog.NOTIFICATION_MEMORY_USE,
+            null,
+            backgroundExecutor,
+            this
+        )
+    }
+
+    /** Called by statsd to pull data. */
+    override fun onPullAtom(atomTag: Int, data: MutableList<StatsEvent>): Int =
+        traceSection("NML#onPullAtom") {
+            if (atomTag != SysUiStatsLog.NOTIFICATION_MEMORY_USE) {
+                return StatsManager.PULL_SKIP
+            }
+
+            // Notifications can only be retrieved on the main thread, so switch to that thread.
+            val notifications = getAllNotificationsOnMainThread()
+            val notificationMemoryUse =
+                NotificationMemoryMeter.notificationMemoryUse(notifications)
+                    .sortedWith(
+                        compareBy(
+                            { it.packageName },
+                            { it.objectUsage.style },
+                            { it.notificationKey }
+                        )
+                    )
+            val usageData = aggregateMemoryUsageData(notificationMemoryUse)
+            usageData.forEach { (_, use) ->
+                data.add(
+                    SysUiStatsLog.buildStatsEvent(
+                        SysUiStatsLog.NOTIFICATION_MEMORY_USE,
+                        use.uid,
+                        use.style,
+                        use.count,
+                        use.countWithInflatedViews,
+                        toKb(use.smallIconObject),
+                        use.smallIconBitmapCount,
+                        toKb(use.largeIconObject),
+                        use.largeIconBitmapCount,
+                        toKb(use.bigPictureObject),
+                        use.bigPictureBitmapCount,
+                        toKb(use.extras),
+                        toKb(use.extenders),
+                        toKb(use.smallIconViews),
+                        toKb(use.largeIconViews),
+                        toKb(use.systemIconViews),
+                        toKb(use.styleViews),
+                        toKb(use.customViews),
+                        toKb(use.softwareBitmaps),
+                        use.seenCount
+                    )
+                )
+            }
+
+            return StatsManager.PULL_SUCCESS
+        }
+
+    private fun getAllNotificationsOnMainThread() =
+        runBlocking(mainDispatcher) {
+            traceSection("NML#getNotifications") { notificationPipeline.allNotifs }
+        }
+
+    /** Aggregates memory usage data by package and style, returning sums. */
+    private fun aggregateMemoryUsageData(
+        notificationMemoryUse: List<NotificationMemoryUsage>
+    ): Map<Pair<String, Int>, NotificationMemoryUseAtomBuilder> {
+        return notificationMemoryUse
+            .groupingBy { Pair(it.packageName, it.objectUsage.style) }
+            .aggregate {
+                _,
+                accumulator: NotificationMemoryUseAtomBuilder?,
+                element: NotificationMemoryUsage,
+                first ->
+                val use =
+                    if (first) {
+                        NotificationMemoryUseAtomBuilder(element.uid, element.objectUsage.style)
+                    } else {
+                        accumulator!!
+                    }
+
+                use.count++
+                // If the views of the notification weren't inflated, the list of memory usage
+                // parameters will be empty.
+                if (element.viewUsage.isNotEmpty()) {
+                    use.countWithInflatedViews++
+                }
+
+                use.smallIconObject += element.objectUsage.smallIcon
+                if (element.objectUsage.smallIcon > 0) {
+                    use.smallIconBitmapCount++
+                }
+
+                use.largeIconObject += element.objectUsage.largeIcon
+                if (element.objectUsage.largeIcon > 0) {
+                    use.largeIconBitmapCount++
+                }
+
+                use.bigPictureObject += element.objectUsage.bigPicture
+                if (element.objectUsage.bigPicture > 0) {
+                    use.bigPictureBitmapCount++
+                }
+
+                use.extras += element.objectUsage.extras
+                use.extenders += element.objectUsage.extender
+
+                // Use totals count which are more accurate when aggregated
+                // in this manner.
+                element.viewUsage
+                    .firstOrNull { vu -> vu.viewType == ViewType.TOTAL }
+                    ?.let {
+                        use.smallIconViews += it.smallIcon
+                        use.largeIconViews += it.largeIcon
+                        use.systemIconViews += it.systemIcons
+                        use.styleViews += it.style
+                        use.customViews += it.style
+                        use.softwareBitmaps += it.softwareBitmapsPenalty
+                    }
+
+                return@aggregate use
+            }
+    }
+
+    /** Rounds the passed value to the nearest KB - e.g. 700B rounds to 1KB. */
+    private fun toKb(value: Int): Int = (value.toFloat() / 1024f).roundToInt()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt
index 7d39e18..41fb91e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt
@@ -1,12 +1,20 @@
 package com.android.systemui.statusbar.notification.logging
 
 import android.app.Notification
+import android.app.Notification.BigPictureStyle
+import android.app.Notification.BigTextStyle
+import android.app.Notification.CallStyle
+import android.app.Notification.DecoratedCustomViewStyle
+import android.app.Notification.InboxStyle
+import android.app.Notification.MediaStyle
+import android.app.Notification.MessagingStyle
 import android.app.Person
 import android.graphics.Bitmap
 import android.graphics.drawable.Icon
 import android.os.Bundle
 import android.os.Parcel
 import android.os.Parcelable
+import android.stats.sysui.NotificationEnums
 import androidx.annotation.WorkerThread
 import com.android.systemui.statusbar.notification.NotificationUtils
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -19,6 +27,7 @@
     private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
     private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
     private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+    private const val AUTOGROUP_KEY = "ranker_group"
 
     /** Returns a list of memory use entries for currently shown notifications. */
     @WorkerThread
@@ -29,12 +38,15 @@
             .asSequence()
             .map { entry ->
                 val packageName = entry.sbn.packageName
+                val uid = entry.sbn.uid
                 val notificationObjectUsage =
                     notificationMemoryUse(entry.sbn.notification, hashSetOf())
                 val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row)
                 NotificationMemoryUsage(
                     packageName,
+                    uid,
                     NotificationUtils.logKey(entry.sbn.key),
+                    entry.sbn.notification,
                     notificationObjectUsage,
                     notificationViewUsage
                 )
@@ -49,7 +61,9 @@
     ): NotificationMemoryUsage {
         return NotificationMemoryUsage(
             entry.sbn.packageName,
+            entry.sbn.uid,
             NotificationUtils.logKey(entry.sbn.key),
+            entry.sbn.notification,
             notificationMemoryUse(entry.sbn.notification, seenBitmaps),
             NotificationMemoryViewWalker.getViewUsage(entry.row)
         )
@@ -116,7 +130,13 @@
         val wearExtenderBackground =
             computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
 
-        val style = notification.notificationStyle
+        val style =
+            if (notification.group == AUTOGROUP_KEY) {
+                NotificationEnums.STYLE_RANKER_GROUP
+            } else {
+                styleEnum(notification.notificationStyle)
+            }
+
         val hasCustomView = notification.contentView != null || notification.bigContentView != null
         val extrasSize = computeBundleSize(extras)
 
@@ -124,7 +144,7 @@
             smallIcon = smallIconUse,
             largeIcon = largeIconUse,
             extras = extrasSize,
-            style = style?.simpleName,
+            style = style,
             styleIcon =
                 bigPictureIconUse +
                     peopleUse +
@@ -144,6 +164,25 @@
     }
 
     /**
+     * Returns logging style enum based on current style class.
+     *
+     * @return style value in [NotificationEnums]
+     */
+    private fun styleEnum(style: Class<out Notification.Style>?): Int =
+        when (style?.name) {
+            null -> NotificationEnums.STYLE_NONE
+            BigTextStyle::class.java.name -> NotificationEnums.STYLE_BIG_TEXT
+            BigPictureStyle::class.java.name -> NotificationEnums.STYLE_BIG_PICTURE
+            InboxStyle::class.java.name -> NotificationEnums.STYLE_INBOX
+            MediaStyle::class.java.name -> NotificationEnums.STYLE_MEDIA
+            DecoratedCustomViewStyle::class.java.name ->
+                NotificationEnums.STYLE_DECORATED_CUSTOM_VIEW
+            MessagingStyle::class.java.name -> NotificationEnums.STYLE_MESSAGING
+            CallStyle::class.java.name -> NotificationEnums.STYLE_CALL
+            else -> NotificationEnums.STYLE_UNSPECIFIED
+        }
+
+    /**
      * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
      * bitmaps). Can be slow.
      */
@@ -176,7 +215,7 @@
      *
      * @return memory usage in bytes or 0 if the icon is Uri/Resource based
      */
-    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
+    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>): Int =
         when (icon?.type) {
             Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
             Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
index c09cc43..f38c1e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -18,11 +18,10 @@
 package com.android.systemui.statusbar.notification.logging
 
 import android.util.Log
-import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.collection.NotifPipeline
-import java.io.PrintWriter
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import dagger.Lazy
 import javax.inject.Inject
 
 /** This class monitors and logs current Notification memory use. */
@@ -30,9 +29,10 @@
 class NotificationMemoryMonitor
 @Inject
 constructor(
-    val notificationPipeline: NotifPipeline,
-    val dumpManager: DumpManager,
-) : Dumpable {
+    private val featureFlags: FeatureFlags,
+    private val notificationMemoryDumper: NotificationMemoryDumper,
+    private val notificationMemoryLogger: Lazy<NotificationMemoryLogger>,
+) {
 
     companion object {
         private const val TAG = "NotificationMemory"
@@ -40,127 +40,10 @@
 
     fun init() {
         Log.d(TAG, "NotificationMemoryMonitor initialized.")
-        dumpManager.registerDumpable(javaClass.simpleName, this)
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        val memoryUse =
-            NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs)
-                .sortedWith(compareBy({ it.packageName }, { it.notificationKey }))
-        dumpNotificationObjects(pw, memoryUse)
-        dumpNotificationViewUsage(pw, memoryUse)
-    }
-
-    /** Renders a table of notification object usage into passed [PrintWriter]. */
-    private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) {
-        pw.println("Notification Object Usage")
-        pw.println("-----------")
-        pw.println(
-            "Package".padEnd(35) +
-                "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom"
-        )
-        pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView")
-        pw.println()
-
-        memoryUse.forEach { use ->
-            pw.println(
-                use.packageName.padEnd(35) +
-                    "\t\t" +
-                    "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" +
-                    (use.objectUsage.style?.take(15) ?: "").padEnd(15) +
-                    "\t\t${use.objectUsage.styleIcon}\t" +
-                    "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" +
-                    "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" +
-                    use.notificationKey
-            )
+        notificationMemoryDumper.init()
+        if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_LOGGING_ENABLED)) {
+            Log.d(TAG, "Notification memory logging enabled.")
+            notificationMemoryLogger.get().init()
         }
-
-        // Calculate totals for easily glanceable summary.
-        data class Totals(
-            var smallIcon: Int = 0,
-            var largeIcon: Int = 0,
-            var styleIcon: Int = 0,
-            var bigPicture: Int = 0,
-            var extender: Int = 0,
-            var extras: Int = 0,
-        )
-
-        val totals =
-            memoryUse.fold(Totals()) { t, usage ->
-                t.smallIcon += usage.objectUsage.smallIcon
-                t.largeIcon += usage.objectUsage.largeIcon
-                t.styleIcon += usage.objectUsage.styleIcon
-                t.bigPicture += usage.objectUsage.bigPicture
-                t.extender += usage.objectUsage.extender
-                t.extras += usage.objectUsage.extras
-                t
-            }
-
-        pw.println()
-        pw.println("TOTALS")
-        pw.println(
-            "".padEnd(35) +
-                "\t\t" +
-                "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" +
-                "".padEnd(15) +
-                "\t\t${toKb(totals.styleIcon)}\t" +
-                "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" +
-                toKb(totals.extras)
-        )
-        pw.println()
-    }
-
-    /** Renders a table of notification view usage into passed [PrintWriter] */
-    private fun dumpNotificationViewUsage(
-        pw: PrintWriter,
-        memoryUse: List<NotificationMemoryUsage>,
-    ) {
-
-        data class Totals(
-            var smallIcon: Int = 0,
-            var largeIcon: Int = 0,
-            var style: Int = 0,
-            var customViews: Int = 0,
-            var softwareBitmapsPenalty: Int = 0,
-        )
-
-        val totals = Totals()
-        pw.println("Notification View Usage")
-        pw.println("-----------")
-        pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware")
-        pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps")
-        pw.println()
-        memoryUse
-            .filter { it.viewUsage.isNotEmpty() }
-            .forEach { use ->
-                pw.println(use.packageName + " " + use.notificationKey)
-                use.viewUsage.forEach { view ->
-                    pw.println(
-                        "  ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" +
-                            "\t${view.largeIcon}\t${view.style}" +
-                            "\t${view.customViews}\t${view.softwareBitmapsPenalty}"
-                    )
-
-                    if (view.viewType == ViewType.TOTAL) {
-                        totals.smallIcon += view.smallIcon
-                        totals.largeIcon += view.largeIcon
-                        totals.style += view.style
-                        totals.customViews += view.customViews
-                        totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty
-                    }
-                }
-            }
-        pw.println()
-        pw.println("TOTALS")
-        pw.println(
-            "  ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" +
-                "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" +
-                "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}"
-        )
-        pw.println()
-    }
-
-    private fun toKb(bytes: Int): String {
-        return (bytes / 1024).toString() + " KB"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
index a0bee15..2d04211 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -50,7 +50,11 @@
 
     /**
      * Returns memory usage of public and private views contained in passed
-     * [ExpandableNotificationRow]
+     * [ExpandableNotificationRow]. Each entry will correspond to one of the [ViewType] values with
+     * [ViewType.TOTAL] totalling all memory use. If a type of view is missing, the corresponding
+     * entry will not appear in resulting list.
+     *
+     * This will return an empty list if the ExpandableNotificationRow has no views inflated.
      */
     fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> {
         if (row == null) {
@@ -58,42 +62,72 @@
         }
 
         // The ordering here is significant since it determines deduplication of seen drawables.
-        return listOf(
-            getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
-            getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild),
-            getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
-            getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout),
-            getTotalUsage(row)
-        )
+        val perViewUsages =
+            listOf(
+                    getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
+                    getViewUsage(
+                        ViewType.PRIVATE_CONTRACTED_VIEW,
+                        row.privateLayout?.contractedChild
+                    ),
+                    getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
+                    getViewUsage(
+                        ViewType.PUBLIC_VIEW,
+                        row.publicLayout?.expandedChild,
+                        row.publicLayout?.contractedChild,
+                        row.publicLayout?.headsUpChild
+                    ),
+                )
+                .filterNotNull()
+
+        return if (perViewUsages.isNotEmpty()) {
+            // Attach summed totals field only if there was any view actually measured.
+            // This reduces bug report noise and makes checks for collapsed views easier.
+            val totals = getTotalUsage(row)
+            if (totals == null) {
+                perViewUsages
+            } else {
+                perViewUsages + totals
+            }
+        } else {
+            listOf()
+        }
     }
 
     /**
      * Calculate total usage of all views - we need to do a separate traversal to make sure we don't
      * double count fields.
      */
-    private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage {
-        val totalUsage = UsageBuilder()
+    private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage? {
         val seenObjects = hashSetOf<Int>()
-
-        row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) }
-        row.privateLayout?.let { child ->
-            for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) {
-                (view as? ViewGroup)?.let { v ->
-                    computeViewHierarchyUse(v, totalUsage, seenObjects)
-                }
-            }
-        }
-        return totalUsage.build(ViewType.TOTAL)
+        return getViewUsage(
+            ViewType.TOTAL,
+            row.privateLayout?.expandedChild,
+            row.privateLayout?.contractedChild,
+            row.privateLayout?.headsUpChild,
+            row.publicLayout?.expandedChild,
+            row.publicLayout?.contractedChild,
+            row.publicLayout?.headsUpChild,
+            seenObjects = seenObjects
+        )
     }
 
     private fun getViewUsage(
         type: ViewType,
-        rootView: View?,
+        vararg rootViews: View?,
         seenObjects: HashSet<Int> = hashSetOf()
-    ): NotificationViewUsage {
-        val usageBuilder = UsageBuilder()
-        (rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) }
-        return usageBuilder.build(type)
+    ): NotificationViewUsage? {
+        val usageBuilder = lazy { UsageBuilder() }
+        rootViews.forEach { rootView ->
+            (rootView as? ViewGroup)?.let { rootViewGroup ->
+                computeViewHierarchyUse(rootViewGroup, usageBuilder.value, seenObjects)
+            }
+        }
+
+        return if (usageBuilder.isInitialized()) {
+            usageBuilder.value.build(type)
+        } else {
+            null
+        }
     }
 
     private fun computeViewHierarchyUse(
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 aff7b4c..b6cf948 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
@@ -871,8 +871,7 @@
         }
 
         for (int i = childCount - 1; i >= 0; i--) {
-            childrenOnTop = updateChildZValue(i, childrenOnTop,
-                    algorithmState, ambientState, i == topHunIndex);
+            updateChildZValue(i, algorithmState, ambientState, i == topHunIndex);
         }
     }
 
@@ -882,15 +881,11 @@
      *
      * @param isTopHun      Whether the child is a top HUN. A top HUN means a HUN that shows on the
      *                      vertically top of screen. Top HUNs should have drop shadows
-     * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated
-     * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height
-     *                      that overlaps with QQS Panel. The integer part represents the count of
-     *                      previous HUNs whose Z positions are greater than 0.
      */
-    protected float updateChildZValue(int i, float childrenOnTop,
-                                      StackScrollAlgorithmState algorithmState,
-                                      AmbientState ambientState,
-                                      boolean isTopHun) {
+    protected void updateChildZValue(int i,
+                                     StackScrollAlgorithmState algorithmState,
+                                     AmbientState ambientState,
+                                     boolean isTopHun) {
         ExpandableView child = algorithmState.visibleChildren.get(i);
         ExpandableViewState childViewState = child.getViewState();
         float baseZ = ambientState.getBaseZHeight();
@@ -904,22 +899,16 @@
             // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0
             // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel.
             // When scrolling down shade to make HUN back to in-position in Notification Panel,
-            // The over-lapping fraction goes to 0, and shadows hides gradually.
-            if (childrenOnTop != 0.0f) {
-                // To elevate the later HUN over previous HUN
-                childrenOnTop++;
-            } else {
-                float overlap = ambientState.getTopPadding()
-                        + ambientState.getStackTranslation() - childViewState.getYTranslation();
-                // To prevent over-shadow during HUN entry
-                childrenOnTop += Math.min(
-                        1.0f,
-                        overlap / childViewState.height
-                );
-                MathUtils.saturate(childrenOnTop);
+            // the overlapFraction goes to 0, and the pinned HUN's shadows hides gradually.
+            float overlap = ambientState.getTopPadding()
+                    + ambientState.getStackTranslation() - childViewState.getYTranslation();
+
+            if (childViewState.height > 0) { // To avoid 0/0 problems
+                // To prevent over-shadow
+                float overlapFraction = MathUtils.saturate(overlap / childViewState.height);
+                childViewState.setZTranslation(baseZ
+                        + overlapFraction * mPinnedZTranslationExtra);
             }
-            childViewState.setZTranslation(baseZ
-                    + childrenOnTop * mPinnedZTranslationExtra);
         } else if (isTopHun) {
             // In case this is a new view that has never been measured before, we don't want to
             // elevate if we are currently expanded more than the notification
@@ -947,15 +936,14 @@
         }
 
         // Handles HUN shadow when shade is closed.
-        // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays.
+        // While shade is closed, and during HUN's entry: headerVisibleAmount stays 0, shadow stays.
+        // While shade is closed, and HUN is showing: headerVisibleAmount stays 0, shadow stays.
         // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes
         // gradually from 0 to 1, shadow hides gradually.
         // Header visibility is a deprecated concept, we are using headerVisibleAmount only because
         // this value nicely goes from 0 to 1 during the HUN-to-Shade process.
-
         childViewState.setZTranslation(childViewState.getZTranslation()
                 + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra);
-        return childrenOnTop;
     }
 
     public void setIsExpanded(boolean isExpanded) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 6b72e96..936589c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -371,6 +371,7 @@
 
         if (!mKeyguardStateController.isShowing()) {
             final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext);
+            cameraIntent.putExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, source);
             mCentralSurfaces.startActivityDismissingKeyguard(cameraIntent,
                     false /* onlyProvisioned */, true /* dismissShade */,
                     true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index d2be8f3..65946c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.app.StatusBarManager.DISABLE_HOME;
 import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
 import static android.app.StatusBarManager.WindowVisibleState;
@@ -69,6 +70,7 @@
 import android.hardware.devicestate.DeviceStateManager;
 import android.metrics.LogMaker;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
@@ -1035,8 +1037,21 @@
         // set the initial view visibility
         int disabledFlags1 = result.mDisabledFlags1;
         int disabledFlags2 = result.mDisabledFlags2;
-        mInitController.addPostInitTask(
-                () -> setUpDisableFlags(disabledFlags1, disabledFlags2));
+        mInitController.addPostInitTask(() -> {
+            setUpDisableFlags(disabledFlags1, disabledFlags2);
+            try {
+                // NOTE(b/262059863): Force-update the disable flags after applying the flags
+                // returned from registerStatusBar(). The result's disabled flags may be stale
+                // if StatusBarManager's disabled flags are updated between registering the bar and
+                // this handling this post-init task. We force an update in this case, and use a new
+                // token to not conflict with any other disabled flags already requested by SysUI
+                Binder token = new Binder();
+                mBarService.disable(DISABLE_HOME, token, mContext.getPackageName());
+                mBarService.disable(0, token, mContext.getPackageName());
+            } catch (RemoteException ex) {
+                ex.rethrowFromSystemServer();
+            }
+        });
 
         mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
index 78b28d2..2ce1163 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
@@ -23,7 +23,7 @@
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import android.widget.FrameLayout
-import com.android.keyguard.KeyguardUpdateMonitor
+import androidx.annotation.StringRes
 import com.android.keyguard.LockIconViewController
 import com.android.systemui.R
 import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
@@ -51,21 +51,29 @@
         defStyleRes,
     ) {
 
+    interface MessageDisplayer {
+        fun display(@StringRes stringResourceId: Int)
+    }
+
     private var ambientIndicationArea: View? = null
     private lateinit var binding: KeyguardBottomAreaViewBinder.Binding
-    private lateinit var lockIconViewController: LockIconViewController
+    private var lockIconViewController: LockIconViewController? = null
 
     /** Initializes the view. */
     fun init(
         viewModel: KeyguardBottomAreaViewModel,
-        falsingManager: FalsingManager,
-        lockIconViewController: LockIconViewController,
+        falsingManager: FalsingManager? = null,
+        lockIconViewController: LockIconViewController? = null,
+        messageDisplayer: MessageDisplayer? = null,
     ) {
-        binding = bind(
+        binding =
+            bind(
                 this,
                 viewModel,
                 falsingManager,
-        )
+            ) {
+                messageDisplayer?.display(it)
+            }
         this.lockIconViewController = lockIconViewController
     }
 
@@ -129,21 +137,21 @@
         findViewById<View>(R.id.ambient_indication_container)?.let {
             val (ambientLeft, ambientTop) = it.locationOnScreen
             if (binding.shouldConstrainToTopOfLockIcon()) {
-                //make top of ambient indication view the bottom of the lock icon
+                // make top of ambient indication view the bottom of the lock icon
                 it.layout(
-                        ambientLeft,
-                        lockIconViewController.bottom.toInt(),
-                        right - ambientLeft,
-                        ambientTop + it.measuredHeight
+                    ambientLeft,
+                    lockIconViewController?.bottom?.toInt() ?: 0,
+                    right - ambientLeft,
+                    ambientTop + it.measuredHeight
                 )
             } else {
-                //make bottom of ambient indication view the top of the lock icon
-                val lockLocationTop = lockIconViewController.top
+                // make bottom of ambient indication view the top of the lock icon
+                val lockLocationTop = lockIconViewController?.top ?: 0
                 it.layout(
-                        ambientLeft,
-                        lockLocationTop.toInt() - it.measuredHeight,
-                        right - ambientLeft,
-                        lockLocationTop.toInt()
+                    ambientLeft,
+                    lockLocationTop.toInt() - it.measuredHeight,
+                    right - ambientLeft,
+                    lockLocationTop.toInt()
                 )
             }
         }
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 f5b5950..cc67c84 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
@@ -46,6 +46,7 @@
  * view-model to be reused for multiple view/view-binder bindings.
  */
 @OptIn(InternalCoroutinesApi::class)
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 object WifiViewBinder {
 
     /**
@@ -59,6 +60,12 @@
 
         /** Notifies that the visibility state has changed. */
         fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int)
+
+        /** Notifies that the icon tint has been updated. */
+        fun onIconTintChanged(newTint: Int)
+
+        /** Notifies that the decor tint has been updated (used only for the dot). */
+        fun onDecorTintChanged(newTint: Int)
     }
 
     /** Binds the view to the view-model, continuing to update the former based on the latter. */
@@ -82,6 +89,9 @@
         @StatusBarIconView.VisibleState
         val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
 
+        val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
+        val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
+
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
@@ -101,7 +111,7 @@
                 }
 
                 launch {
-                    viewModel.tint.collect { tint ->
+                    iconTint.collect { tint ->
                         val tintList = ColorStateList.valueOf(tint)
                         iconView.imageTintList = tintList
                         activityInView.imageTintList = tintList
@@ -110,6 +120,8 @@
                     }
                 }
 
+                launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } }
+
                 launch {
                     viewModel.isActivityInViewVisible.distinctUntilChanged().collect { visible ->
                         activityInView.isVisible = visible
@@ -144,6 +156,20 @@
             override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
                 visibilityState.value = state
             }
+
+            override fun onIconTintChanged(newTint: Int) {
+                if (viewModel.useDebugColoring) {
+                    return
+                }
+                iconTint.value = newTint
+            }
+
+            override fun onDecorTintChanged(newTint: Int) {
+                if (viewModel.useDebugColoring) {
+                    return
+                }
+                decorTint.value = newTint
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
index a45076b..be7782c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -22,6 +22,7 @@
 import android.view.Gravity
 import android.view.LayoutInflater
 import com.android.systemui.R
+import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.statusbar.BaseStatusBarFrameLayout
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
@@ -51,18 +52,20 @@
             binding.onVisibilityStateChanged(value)
         }
 
-    override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
-        // TODO(b/238425913)
-    }
-
     override fun getSlot() = slot
 
+    override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
+        val newTint = DarkIconDispatcher.getTint(areas, this, tint)
+        binding.onIconTintChanged(newTint)
+        binding.onDecorTintChanged(newTint)
+    }
+
     override fun setStaticDrawableColor(color: Int) {
-        // TODO(b/238425913)
+        binding.onIconTintChanged(color)
     }
 
     override fun setDecorColor(color: Int) {
-        // TODO(b/238425913)
+        binding.onDecorTintChanged(color)
     }
 
     override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: 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 e35a8fe..a4615cc 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
@@ -21,7 +21,6 @@
 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
 
 /**
  * A view model for a wifi icon in a specific location. This allows us to control parameters that
@@ -48,24 +47,12 @@
     /** True if the airplane spacer view should be visible. */
     val isAirplaneSpacerVisible: Flow<Boolean>,
 ) {
-    /** The color that should be used to tint the icon. */
-    val tint: Flow<Int> =
-        flowOf(
-            if (statusBarPipelineFlags.useWifiDebugColoring()) {
-                debugTint
-            } else {
-                DEFAULT_TINT
-            }
-        )
+    val useDebugColoring: Boolean = statusBarPipelineFlags.useWifiDebugColoring()
 
-    companion object {
-        /**
-         * A default icon tint.
-         *
-         * TODO(b/238425913): The tint is actually controlled by
-         * [com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager]. We
-         * should use that logic instead of white as a default.
-         */
-        private const val DEFAULT_TINT = Color.WHITE
-    }
+    val defaultColor: Int =
+        if (useDebugColoring) {
+            debugTint
+        } else {
+            Color.WHITE
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
new file mode 100644
index 0000000..7ccb316
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
@@ -0,0 +1,8 @@
+# Bug component: 1254381
+azappone@google.com
+achalke@google.com
+juliacr@google.com
+madym@google.com
+mgalhardo@google.com
+petrcermak@google.com
+vanjan@google.com
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index ea40208..db7315f 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.wakelock.WakeLock
 
 /**
@@ -44,8 +45,24 @@
  *
  * The generic type T is expected to contain all the information necessary for the subclasses to
  * display the view in a certain state, since they receive <T> in [updateView].
+ *
+ * Some information about display ordering:
+ *
+ * [ViewPriority] defines different priorities for the incoming views. The incoming view will be
+ * displayed so long as its priority is equal to or greater than the currently displayed view.
+ * (Concretely, this means that a [ViewPriority.NORMAL] won't be displayed if a
+ * [ViewPriority.CRITICAL] is currently displayed. But otherwise, the incoming view will get
+ * displayed and kick out the old view).
+ *
+ * Once the currently displayed view times out, we *may* display a previously requested view if it
+ * still has enough time left before its own timeout. The same priority ordering applies.
+ *
+ * Note: [TemporaryViewInfo.id] is the identifier that we use to determine if a call to
+ * [displayView] will just update the current view with new information, or display a completely new
+ * view. This means that you *cannot* change the [TemporaryViewInfo.priority] or
+ * [TemporaryViewInfo.windowTitle] while using the same ID.
  */
-abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger>(
+abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger<T>>(
     internal val context: Context,
     internal val logger: U,
     internal val windowManager: WindowManager,
@@ -55,6 +72,7 @@
     private val powerManager: PowerManager,
     @LayoutRes private val viewLayoutRes: Int,
     private val wakeLockBuilder: WakeLock.Builder,
+    private val systemClock: SystemClock,
 ) : CoreStartable {
     /**
      * Window layout params that will be used as a starting point for the [windowLayoutParams] of
@@ -78,27 +96,18 @@
      */
     internal abstract val windowLayoutParams: WindowManager.LayoutParams
 
-    /** A container for all the display-related objects. Null if the view is not being displayed. */
-    private var displayInfo: DisplayInfo? = null
-
-    /** A [Runnable] that, when run, will cancel the pending timeout of the view. */
-    private var cancelViewTimeout: Runnable? = null
-
     /**
-     * A wakelock that is acquired when view is displayed and screen off,
-     * then released when view is removed.
+     * A list of the currently active views, ordered from highest priority in the beginning to
+     * lowest priority at the end.
+     *
+     * Whenever the current view disappears, the next-priority view will be displayed if it's still
+     * valid.
      */
-    private var wakeLock: WakeLock? = null
+    internal val activeViews: MutableList<DisplayInfo> = mutableListOf()
 
-    /** A string that keeps track of wakelock reason once it is acquired till it gets released */
-    private var wakeReasonAcquired: String? = null
-
-    /**
-     * A stack of pairs of device id and temporary view info. This is used when there may be
-     * multiple devices in range, and we want to always display the chip for the most recently
-     * active device.
-     */
-    internal val activeViews: ArrayDeque<Pair<String, T>> = ArrayDeque()
+    private fun getCurrentDisplayInfo(): DisplayInfo? {
+        return activeViews.getOrNull(0)
+    }
 
     /**
      * Displays the view with the provided [newInfo].
@@ -107,94 +116,139 @@
      * display the correct information in the view.
      * @param onViewTimeout a runnable that runs after the view timeout.
      */
+    @Synchronized
     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
-        // front of the list
-        val id = newInfo.id
-        val position = findAndRemoveFromActiveViewsList(id)
-        activeViews.addFirst(Pair(id, newInfo))
-
-        if (currentDisplayInfo != null &&
-            currentDisplayInfo.info.windowTitle == newInfo.windowTitle) {
-            // We're already displaying information in the correctly-titled window, so we just need
-            // to update the view.
-            currentDisplayInfo.info = newInfo
-            updateView(currentDisplayInfo.info, currentDisplayInfo.view)
-        } else {
-            if (currentDisplayInfo != null) {
-                // We're already displaying information but that information is under a different
-                // window title. So, we need to remove the old window with the old title and add a
-                // new window with the new title.
-                removeView(
-                    id,
-                    removalReason = "New info has new window title: ${newInfo.windowTitle}"
-                )
-            }
-
-            // At this point, we're guaranteed to no longer be displaying a view.
-            // So, set up all our callbacks and inflate the view.
-            configurationController.addCallback(displayScaleListener)
-
-            wakeLock = if (!powerManager.isScreenOn) {
-                // If the screen is off, fully wake it so the user can see the view.
-                wakeLockBuilder
-                    .setTag(newInfo.windowTitle)
-                    .setLevelsAndFlags(
-                            PowerManager.FULL_WAKE_LOCK or
-                                PowerManager.ACQUIRE_CAUSES_WAKEUP
-                    )
-                    .build()
-            } else {
-                // Per b/239426653, we want the view to show over the dream state.
-                // If the screen is on, using screen bright level will leave screen on the dream
-                // state but ensure the screen will not go off before wake lock is released.
-                wakeLockBuilder
-                    .setTag(newInfo.windowTitle)
-                    .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK)
-                    .build()
-            }
-            wakeLock?.acquire(newInfo.wakeReason)
-            wakeReasonAcquired = newInfo.wakeReason
-            logger.logViewAddition(id, newInfo.windowTitle)
-            inflateAndUpdateView(newInfo)
-        }
-
-        // Cancel and re-set the view timeout each time we get a new state.
         val timeout = accessibilityManager.getRecommendedTimeoutMillis(
             newInfo.timeoutMs,
             // Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
             // include it just to be safe.
             FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS
-       )
+        )
+        val timeExpirationMillis = systemClock.currentTimeMillis() + timeout
 
-        // Only cancel timeout of the most recent view displayed, as it will be reset.
-        if (position == 0) {
-            cancelViewTimeout?.run()
+        val currentDisplayInfo = getCurrentDisplayInfo()
+
+        // We're current displaying a chipbar with the same ID, we just need to update its info
+        if (currentDisplayInfo != null && currentDisplayInfo.info.id == newInfo.id) {
+            val view = checkNotNull(currentDisplayInfo.view) {
+                "First item in activeViews list must have a valid view"
+            }
+            logger.logViewUpdate(newInfo)
+            currentDisplayInfo.info = newInfo
+            currentDisplayInfo.timeExpirationMillis = timeExpirationMillis
+            updateTimeout(currentDisplayInfo, timeout, onViewTimeout)
+            updateView(newInfo, view)
+            return
         }
-        cancelViewTimeout = mainExecutor.executeDelayed(
+
+        val newDisplayInfo = DisplayInfo(
+            info = newInfo,
+            onViewTimeout = onViewTimeout,
+            timeExpirationMillis = timeExpirationMillis,
+            // Null values will be updated to non-null if/when this view actually gets displayed
+            view = null,
+            wakeLock = null,
+            cancelViewTimeout = null,
+        )
+
+        // We're not displaying anything, so just render this new info
+        if (currentDisplayInfo == null) {
+            addCallbacks()
+            activeViews.add(newDisplayInfo)
+            showNewView(newDisplayInfo, timeout)
+            return
+        }
+
+        // The currently displayed info takes higher priority than the new one.
+        // So, just store the new one in case the current one disappears.
+        if (currentDisplayInfo.info.priority > newInfo.priority) {
+            logger.logViewAdditionDelayed(newInfo)
+            // Remove any old information for this id (if it exists) and re-add it to the list in
+            // the right priority spot
+            removeFromActivesIfNeeded(newInfo.id)
+            var insertIndex = 0
+            while (insertIndex < activeViews.size &&
+                activeViews[insertIndex].info.priority > newInfo.priority) {
+                insertIndex++
+            }
+            activeViews.add(insertIndex, newDisplayInfo)
+            return
+        }
+
+        // Else: The newInfo should be displayed and the currentInfo should be hidden
+        hideView(currentDisplayInfo)
+        // Remove any old information for this id (if it exists) and put this info at the beginning
+        removeFromActivesIfNeeded(newDisplayInfo.info.id)
+        activeViews.add(0, newDisplayInfo)
+        showNewView(newDisplayInfo, timeout)
+    }
+
+    private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) {
+        logger.logViewAddition(newDisplayInfo.info)
+        createAndAcquireWakeLock(newDisplayInfo)
+        updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout)
+        inflateAndUpdateView(newDisplayInfo)
+    }
+
+    private fun createAndAcquireWakeLock(displayInfo: DisplayInfo) {
+        // TODO(b/262009503): Migrate off of isScrenOn, since it's deprecated.
+        val newWakeLock = if (!powerManager.isScreenOn) {
+            // If the screen is off, fully wake it so the user can see the view.
+            wakeLockBuilder
+                .setTag(displayInfo.info.windowTitle)
+                .setLevelsAndFlags(
+                    PowerManager.FULL_WAKE_LOCK or
+                        PowerManager.ACQUIRE_CAUSES_WAKEUP
+                )
+                .build()
+        } else {
+            // Per b/239426653, we want the view to show over the dream state.
+            // If the screen is on, using screen bright level will leave screen on the dream
+            // state but ensure the screen will not go off before wake lock is released.
+            wakeLockBuilder
+                .setTag(displayInfo.info.windowTitle)
+                .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK)
+                .build()
+        }
+        displayInfo.wakeLock = newWakeLock
+        newWakeLock.acquire(displayInfo.info.wakeReason)
+    }
+
+    /**
+     * Creates a runnable that will remove [displayInfo] in [timeout] ms from now.
+     *
+     * @param onViewTimeout an optional runnable that will be run if the view times out.
+     * @return a runnable that, when run, will *cancel* the view's timeout.
+     */
+    private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int, onViewTimeout: Runnable?) {
+        val cancelViewTimeout = mainExecutor.executeDelayed(
             {
-                removeView(id, REMOVAL_REASON_TIMEOUT)
+                removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT)
                 onViewTimeout?.run()
             },
             timeout.toLong()
         )
+
+        displayInfo.onViewTimeout = onViewTimeout
+        // Cancel old view timeout and re-set it.
+        displayInfo.cancelViewTimeout?.run()
+        displayInfo.cancelViewTimeout = cancelViewTimeout
     }
 
-    /** Inflates a new view, updates it with [newInfo], and adds the view to the window. */
-    private fun inflateAndUpdateView(newInfo: T) {
+    /** Inflates a new view, updates it with [DisplayInfo.info], and adds the view to the window. */
+    private fun inflateAndUpdateView(displayInfo: DisplayInfo) {
+        val newInfo = displayInfo.info
         val newView = LayoutInflater
                 .from(context)
                 .inflate(viewLayoutRes, null) as ViewGroup
-        val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion)
-        newViewController.init()
+        displayInfo.view = newView
 
         // We don't need to hold on to the view controller since we never set anything additional
         // on it -- it will be automatically cleaned up when the view is detached.
-        val newDisplayInfo = DisplayInfo(newView, newInfo)
-        displayInfo = newDisplayInfo
-        updateView(newDisplayInfo.info, newDisplayInfo.view)
+        val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion)
+        newViewController.init()
+
+        updateView(newInfo, newView)
 
         val paramsWithTitle = WindowManager.LayoutParams().also {
             it.copyFrom(windowLayoutParams)
@@ -206,11 +260,15 @@
     }
 
     /** Removes then re-inflates the view. */
+    @Synchronized
     private fun reinflateView() {
-        val currentViewInfo = displayInfo ?: return
+        val currentDisplayInfo = getCurrentDisplayInfo() ?: return
 
-        windowManager.removeView(currentViewInfo.view)
-        inflateAndUpdateView(currentViewInfo.info)
+        val view = checkNotNull(currentDisplayInfo.view) {
+            "First item in activeViews list must have a valid view"
+        }
+        windowManager.removeView(view)
+        inflateAndUpdateView(currentDisplayInfo)
     }
 
     private val displayScaleListener = object : ConfigurationController.ConfigurationListener {
@@ -219,68 +277,109 @@
         }
     }
 
+    private fun addCallbacks() {
+        configurationController.addCallback(displayScaleListener)
+    }
+
+    private fun removeCallbacks() {
+        configurationController.removeCallback(displayScaleListener)
+    }
+
     /**
-     * Hides the view given its [id].
+     * Completely removes the view for the given [id], both visually and from our internal store.
      *
      * @param id the id of the device responsible of displaying the temp view.
      * @param removalReason a short string describing why the view was removed (timeout, state
      *     change, etc.)
      */
+    @Synchronized
     fun removeView(id: String, removalReason: String) {
-        val currentDisplayInfo = displayInfo ?: return
-
-        val removalPosition = findAndRemoveFromActiveViewsList(id)
-        if (removalPosition == null) {
-            logger.logViewRemovalIgnored(id, "view not found in the list")
-            return
-        }
-        if (removalPosition != 0) {
-            logger.logViewRemovalIgnored(id, "most recent view is being displayed.")
-            return
-        }
         logger.logViewRemoval(id, removalReason)
 
-        val newViewToDisplay = if (activeViews.isEmpty()) {
-            null
-        } else {
-            activeViews[0].second
+        val displayInfo = activeViews.firstOrNull { it.info.id == id }
+        if (displayInfo == null) {
+            logger.logViewRemovalIgnored(id, "View not found in list")
+            return
         }
 
-        val currentView = currentDisplayInfo.view
-        animateViewOut(currentView) {
-            windowManager.removeView(currentView)
-            wakeLock?.release(wakeReasonAcquired)
-        }
+        val currentlyDisplayedView = activeViews[0]
+        // Remove immediately (instead as part of the animation end runnable) so that if a new view
+        // event comes in while this view is animating out, we still display the new view
+        // appropriately.
+        activeViews.remove(displayInfo)
 
-        configurationController.removeCallback(displayScaleListener)
-        // Re-set to null immediately (instead as part of the animation end runnable) so
-        // that if a new view event comes in while this view is animating out, we still display
-        // the new view appropriately.
-        displayInfo = null
         // No need to time the view out since it's already gone
-        cancelViewTimeout?.run()
+        displayInfo.cancelViewTimeout?.run()
+
+        if (displayInfo.view == null) {
+            logger.logViewRemovalIgnored(id, "No view to remove")
+            return
+        }
+
+        if (currentlyDisplayedView.info.id != id) {
+            logger.logViewRemovalIgnored(id, "View isn't the currently displayed view")
+            return
+        }
+
+        removeViewFromWindow(displayInfo)
+
+        // Prune anything that's already timed out before determining if we should re-display a
+        // different chipbar.
+        removeTimedOutViews()
+        val newViewToDisplay = getCurrentDisplayInfo()
 
         if (newViewToDisplay != null) {
-            mainExecutor.executeDelayed({ displayView(newViewToDisplay)}, DISPLAY_VIEW_DELAY)
+            val timeout = newViewToDisplay.timeExpirationMillis - systemClock.currentTimeMillis()
+            // TODO(b/258019006): We may want to have a delay before showing the new view so
+            // that the UI translation looks a bit smoother. But, we expect this to happen
+            // rarely so it may not be worth the extra complexity.
+            showNewView(newViewToDisplay, timeout.toInt())
+        } else {
+            removeCallbacks()
         }
     }
 
     /**
-     * Finds and removes the active view with the given [id] from the stack, or null if there is no
-     * active view with that ID
-     *
-     * @param id that temporary view belonged to.
-     *
-     * @return index of the view in the stack , otherwise null.
+     * Hides the view from the window, but keeps [displayInfo] around in [activeViews] in case it
+     * should be re-displayed later.
      */
-    private fun findAndRemoveFromActiveViewsList(id: String): Int? {
-        for (i in 0 until activeViews.size) {
-            if (activeViews[i].first == id) {
-                activeViews.removeAt(i)
-                return i
-            }
+    private fun hideView(displayInfo: DisplayInfo) {
+        logger.logViewHidden(displayInfo.info)
+        removeViewFromWindow(displayInfo)
+    }
+
+    private fun removeViewFromWindow(displayInfo: DisplayInfo) {
+        val view = displayInfo.view
+        if (view == null) {
+            logger.logViewRemovalIgnored(displayInfo.info.id, "View is null")
+            return
         }
-        return null
+        displayInfo.view = null // Need other places??
+        animateViewOut(view) {
+            windowManager.removeView(view)
+            displayInfo.wakeLock?.release(displayInfo.info.wakeReason)
+        }
+    }
+
+    @Synchronized
+    private fun removeTimedOutViews() {
+        val invalidViews = activeViews
+            .filter { it.timeExpirationMillis <
+                systemClock.currentTimeMillis() + MIN_REQUIRED_TIME_FOR_REDISPLAY }
+
+        invalidViews.forEach {
+            activeViews.remove(it)
+            logger.logViewExpiration(it.info)
+        }
+    }
+
+    @Synchronized
+    private fun removeFromActivesIfNeeded(id: String) {
+        val toRemove = activeViews.find { it.info.id == id }
+        toRemove?.let {
+            it.cancelViewTimeout?.run()
+            activeViews.remove(it)
+        }
     }
 
     /**
@@ -311,17 +410,47 @@
     }
 
     /** A container for all the display-related state objects. */
-    private inner class DisplayInfo(
-        /** The view currently being displayed. */
-        val view: ViewGroup,
+    inner class DisplayInfo(
+        /**
+         * The view currently being displayed.
+         *
+         * Null if this info isn't currently being displayed.
+         */
+        var view: ViewGroup?,
 
-        /** The info currently being displayed. */
+        /** The info that should be displayed if/when this is the highest priority view. */
         var info: T,
+
+        /**
+         * The system time at which this display info should expire and never be displayed again.
+         */
+        var timeExpirationMillis: Long,
+
+        /**
+         * The wake lock currently held by this view. Must be released when the view disappears.
+         *
+         * Null if this info isn't currently being displayed.
+         */
+        var wakeLock: WakeLock?,
+
+        /**
+         * See [displayView].
+         */
+        var onViewTimeout: Runnable?,
+
+        /**
+         * A runnable that, when run, will cancel this view's timeout.
+         *
+         * Null if this info isn't currently being displayed.
+         */
+        var cancelViewTimeout: Runnable?,
     )
+
+    // TODO(b/258019006): Add a dump method that dumps the currently active views.
 }
 
 private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
-const val DISPLAY_VIEW_DELAY = 50L
+private const val MIN_REQUIRED_TIME_FOR_REDISPLAY = 1000
 
 private data class IconInfo(
     val iconName: String,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
index df83960..5596cf6 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
@@ -42,6 +42,20 @@
      * The id of the temporary view.
      */
     abstract val id: String
+
+    /** The priority for this view. */
+    abstract val priority: ViewPriority
 }
 
 const val DEFAULT_TIMEOUT_MILLIS = 10000
+
+/**
+ * The priority of the view being displayed.
+ *
+ * Must be ordered from lowest priority to highest priority. (CRITICAL is currently the highest
+ * priority.)
+ */
+enum class ViewPriority {
+    NORMAL,
+    CRITICAL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index 133a384..ec6965a 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -20,20 +20,79 @@
 import com.android.systemui.plugins.log.LogLevel
 
 /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
-open class TemporaryViewLogger(
+open class TemporaryViewLogger<T : TemporaryViewInfo>(
     internal val buffer: LogBuffer,
     internal val tag: String,
 ) {
-    /** Logs that we added the view with the given [id] in a window titled [windowTitle]. */
-    fun logViewAddition(id: String, windowTitle: String) {
+    fun logViewExpiration(info: T) {
         buffer.log(
             tag,
             LogLevel.DEBUG,
             {
-                str1 = windowTitle
-                str2 = id
+                str1 = info.id
+                str2 = info.windowTitle
+                str3 = info.priority.name
             },
-            { "View added. window=$str1 id=$str2" }
+            { "View timeout has already expired; removing. id=$str1 window=$str2 priority=$str3" }
+        )
+    }
+
+    fun logViewUpdate(info: T) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = info.id
+                str2 = info.windowTitle
+                str3 = info.priority.name
+            },
+            { "Existing view updated with new data. id=$str1 window=$str2 priority=$str3" }
+        )
+    }
+
+    fun logViewAdditionDelayed(info: T) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = info.id
+                str2 = info.windowTitle
+                str3 = info.priority.name
+            },
+            {
+                "New view can't be displayed because higher priority view is currently " +
+                    "displayed. New view id=$str1 window=$str2 priority=$str3"
+            }
+        )
+    }
+
+    /** Logs that we added the view with the given information. */
+    fun logViewAddition(info: T) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = info.id
+                str2 = info.windowTitle
+                str3 = info.priority.name
+            },
+            { "View added. id=$str1 window=$str2 priority=$str3" }
+        )
+    }
+
+    fun logViewHidden(info: T) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = info.id
+                str2 = info.windowTitle
+                str3 = info.priority.name
+            },
+            {
+                "View hidden in favor of newer view. " +
+                    "Hidden view id=$str1 window=$str2 priority=$str3"
+            }
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 4d91e35..14ba63a 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.android.systemui.util.wakelock.WakeLock
 import javax.inject.Inject
@@ -77,6 +78,7 @@
         private val viewUtil: ViewUtil,
         private val vibratorHelper: VibratorHelper,
         wakeLockBuilder: WakeLock.Builder,
+        systemClock: SystemClock,
 ) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>(
         context,
         logger,
@@ -87,6 +89,7 @@
         powerManager,
         R.layout.chipbar,
         wakeLockBuilder,
+        systemClock,
 ) {
 
     private lateinit var parent: ChipbarRootView
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
index a3eef80..dd4bd26 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.shared.model.TintedIcon
 import com.android.systemui.temporarydisplay.TemporaryViewInfo
+import com.android.systemui.temporarydisplay.ViewPriority
 
 /**
  * A container for all the state needed to display a chipbar via [ChipbarCoordinator].
@@ -42,6 +43,7 @@
     override val wakeReason: String,
     override val timeoutMs: Int,
     override val id: String,
+    override val priority: ViewPriority,
 ) : TemporaryViewInfo() {
     companion object {
         @AttrRes const val DEFAULT_ICON_TINT_ATTR = android.R.attr.textColorPrimary
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
index e477cd6..fcfbe0a 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
@@ -29,7 +29,7 @@
 @Inject
 constructor(
     @ChipbarLog buffer: LogBuffer,
-) : TemporaryViewLogger(buffer, "ChipbarLog") {
+) : TemporaryViewLogger<ChipbarInfo>(buffer, "ChipbarLog") {
     /**
      * Logs that the chipbar was updated to display in a window named [windowTitle], with [text] and
      * [endItemDesc].
diff --git a/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java b/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java
index 3d07491..166ac9e 100644
--- a/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java
+++ b/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java
@@ -18,41 +18,58 @@
 
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.util.Log;
+import android.view.AttachedSurfaceControl;
 import android.view.View;
-import android.view.ViewRootImpl;
 
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 
+import com.android.systemui.dagger.qualifiers.Main;
+
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.concurrent.Executor;
 
+import javax.inject.Inject;
+
 /**
  * {@link TouchInsetManager} handles setting the touchable inset regions for a given View. This
  * is useful for passing through touch events for all but select areas.
  */
 public class TouchInsetManager {
+    private static final String TAG = "TouchInsetManager";
     /**
      * {@link TouchInsetSession} provides an individualized session with the
      * {@link TouchInsetManager}, linking any action to the client.
      */
     public static class TouchInsetSession {
         private final TouchInsetManager mManager;
-
         private final HashSet<View> mTrackedViews;
         private final Executor mExecutor;
 
         private final View.OnLayoutChangeListener mOnLayoutChangeListener =
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
-                        -> updateTouchRegion();
+                        -> updateTouchRegions();
+
+        private final View.OnAttachStateChangeListener mAttachListener =
+                new View.OnAttachStateChangeListener() {
+                    @Override
+                    public void onViewAttachedToWindow(View v) {
+                        updateTouchRegions();
+                    }
+
+                    @Override
+                    public void onViewDetachedFromWindow(View v) {
+                        updateTouchRegions();
+                    }
+                };
 
         /**
          * Default constructor
          * @param manager The parent {@link TouchInsetManager} which will be affected by actions on
          *                this session.
-         * @param rootView The parent of views that will be tracked.
          * @param executor An executor for marshalling operations.
          */
         TouchInsetSession(TouchInsetManager manager, Executor executor) {
@@ -68,8 +85,9 @@
         public void addViewToTracking(View view) {
             mExecutor.execute(() -> {
                 mTrackedViews.add(view);
+                view.addOnAttachStateChangeListener(mAttachListener);
                 view.addOnLayoutChangeListener(mOnLayoutChangeListener);
-                updateTouchRegion();
+                updateTouchRegions();
             });
         }
 
@@ -81,22 +99,30 @@
             mExecutor.execute(() -> {
                 mTrackedViews.remove(view);
                 view.removeOnLayoutChangeListener(mOnLayoutChangeListener);
-                updateTouchRegion();
+                view.removeOnAttachStateChangeListener(mAttachListener);
+                updateTouchRegions();
             });
         }
 
-        private void updateTouchRegion() {
-            final Region cumulativeRegion = Region.obtain();
+        private void updateTouchRegions() {
+            mExecutor.execute(() -> {
+                final HashMap<AttachedSurfaceControl, Region> affectedSurfaces = new HashMap<>();
+                mTrackedViews.stream().forEach(view -> {
+                    if (!view.isAttachedToWindow()) {
+                        return;
+                    }
 
-            mTrackedViews.stream().forEach(view -> {
-                final Rect boundaries = new Rect();
-                view.getBoundsOnScreen(boundaries);
-                cumulativeRegion.op(boundaries, Region.Op.UNION);
+                    final AttachedSurfaceControl surface = view.getRootSurfaceControl();
+
+                    if (!affectedSurfaces.containsKey(surface)) {
+                        affectedSurfaces.put(surface, Region.obtain());
+                    }
+                    final Rect boundaries = new Rect();
+                    view.getBoundsOnScreen(boundaries);
+                    affectedSurfaces.get(surface).op(boundaries, Region.Op.UNION);
+                });
+                mManager.setTouchRegions(this, affectedSurfaces);
             });
-
-            mManager.setTouchRegion(this, cumulativeRegion);
-
-            cumulativeRegion.recycle();
         }
 
         /**
@@ -110,32 +136,18 @@
         }
     }
 
-    private final HashMap<TouchInsetSession, Region> mDefinedRegions = new HashMap<>();
+    private final HashMap<TouchInsetSession, HashMap<AttachedSurfaceControl, Region>>
+            mSessionRegions = new HashMap<>();
+    private final HashMap<AttachedSurfaceControl, Region> mLastAffectedSurfaces = new HashMap();
     private final Executor mExecutor;
-    private final View mRootView;
-
-    private final View.OnAttachStateChangeListener mAttachListener =
-            new View.OnAttachStateChangeListener() {
-                @Override
-                public void onViewAttachedToWindow(View v) {
-                    updateTouchInset();
-                }
-
-                @Override
-                public void onViewDetachedFromWindow(View v) {
-                }
-            };
 
     /**
      * Default constructor.
      * @param executor An {@link Executor} to marshal all operations on.
-     * @param rootView The root {@link View} for all views in sessions.
      */
-    public TouchInsetManager(Executor executor, View rootView) {
+    @Inject
+    public TouchInsetManager(@Main Executor executor) {
         mExecutor = executor;
-        mRootView = rootView;
-        mRootView.addOnAttachStateChangeListener(mAttachListener);
-
     }
 
     /**
@@ -151,47 +163,68 @@
     public ListenableFuture<Boolean> checkWithinTouchRegion(int x, int y) {
         return CallbackToFutureAdapter.getFuture(completer -> {
             mExecutor.execute(() -> completer.set(
-                    mDefinedRegions.values().stream().anyMatch(region -> region.contains(x, y))));
+                    mLastAffectedSurfaces.values().stream().anyMatch(
+                            region -> region.contains(x, y))));
 
             return "DreamOverlayTouchMonitor::checkWithinTouchRegion";
         });
     }
 
-    private void updateTouchInset() {
-        final ViewRootImpl viewRootImpl = mRootView.getViewRootImpl();
+    private void updateTouchInsets() {
+        // Get affected
+        final HashMap<AttachedSurfaceControl, Region> affectedSurfaces = new HashMap<>();
+        mSessionRegions.values().stream().forEach(regionMapping -> {
+            regionMapping.entrySet().stream().forEach(entry -> {
+                final AttachedSurfaceControl surface = entry.getKey();
+                if (!affectedSurfaces.containsKey(surface)) {
+                    affectedSurfaces.put(surface, Region.obtain());
+                }
 
-        if (viewRootImpl == null) {
+                affectedSurfaces.get(surface).op(entry.getValue(), Region.Op.UNION);
+            });
+        });
+
+        affectedSurfaces.entrySet().stream().forEach(entry -> {
+            entry.getKey().setTouchableRegion(entry.getValue());
+        });
+
+        mLastAffectedSurfaces.entrySet().forEach(entry -> {
+            final AttachedSurfaceControl surface = entry.getKey();
+            if (!affectedSurfaces.containsKey(surface)) {
+                surface.setTouchableRegion(null);
+            }
+            entry.getValue().recycle();
+        });
+
+        mLastAffectedSurfaces.clear();
+        mLastAffectedSurfaces.putAll(affectedSurfaces);
+    }
+
+    protected void setTouchRegions(TouchInsetSession session,
+            HashMap<AttachedSurfaceControl, Region> regions) {
+        mExecutor.execute(() -> {
+            recycleRegions(session);
+            mSessionRegions.put(session, regions);
+            updateTouchInsets();
+        });
+    }
+
+    private void recycleRegions(TouchInsetSession session) {
+        if (!mSessionRegions.containsKey(session)) {
+            Log.w(TAG,  "Removing a session with no regions:" + session);
             return;
         }
 
-        final Region aggregateRegion = Region.obtain();
-
-        for (Region region : mDefinedRegions.values()) {
-            aggregateRegion.op(region, Region.Op.UNION);
+        for (Region region : mSessionRegions.get(session).values()) {
+            region.recycle();
         }
-
-        viewRootImpl.setTouchableRegion(aggregateRegion);
-
-        aggregateRegion.recycle();
-    }
-
-    protected void setTouchRegion(TouchInsetSession session, Region region) {
-        final Region introducedRegion = Region.obtain(region);
-        mExecutor.execute(() -> {
-            mDefinedRegions.put(session, introducedRegion);
-            updateTouchInset();
-        });
     }
 
     private void clearRegion(TouchInsetSession session) {
         mExecutor.execute(() -> {
-            final Region storedRegion = mDefinedRegions.remove(session);
-
-            if (storedRegion != null) {
-                storedRegion.recycle();
-            }
-
-            updateTouchInset();
+            recycleRegions(session);
+            mSessionRegions.remove(session);
+            updateTouchInsets();
         });
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
index 8bbaf3d..1059543 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
@@ -19,6 +19,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -87,6 +88,7 @@
         when(mAbsKeyInputView.isAttachedToWindow()).thenReturn(true);
         when(mAbsKeyInputView.requireViewById(R.id.bouncer_message_area))
                 .thenReturn(mKeyguardMessageArea);
+        when(mAbsKeyInputView.getResources()).thenReturn(getContext().getResources());
         mKeyguardAbsKeyInputViewController = new KeyguardAbsKeyInputViewController(mAbsKeyInputView,
                 mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
                 mKeyguardMessageAreaControllerFactory, mLatencyTracker, mFalsingCollector,
@@ -99,6 +101,11 @@
             public void onResume(int reason) {
                 super.onResume(reason);
             }
+
+            @Override
+            protected int getInitialMessageResId() {
+                return 0;
+            }
         };
         mKeyguardAbsKeyInputViewController.init();
         reset(mKeyguardMessageAreaController);  // Clear out implicit call to init.
@@ -125,4 +132,22 @@
         verifyZeroInteractions(mKeyguardSecurityCallback);
         verifyZeroInteractions(mKeyguardMessageAreaController);
     }
+
+    @Test
+    public void onPromptReasonNone_doesNotSetMessage() {
+        mKeyguardAbsKeyInputViewController.showPromptReason(0);
+        verify(mKeyguardMessageAreaController, never()).setMessage(
+                getContext().getResources().getString(R.string.kg_prompt_reason_restart_password),
+                false);
+    }
+
+    @Test
+    public void onPromptReason_setsMessage() {
+        when(mAbsKeyInputView.getPromptReasonStringRes(1)).thenReturn(
+                R.string.kg_prompt_reason_restart_password);
+        mKeyguardAbsKeyInputViewController.showPromptReason(1);
+        verify(mKeyguardMessageAreaController).setMessage(
+                getContext().getResources().getString(R.string.kg_prompt_reason_restart_password),
+                false);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 61c7bb5..c8e7538 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -29,7 +29,6 @@
 
 import android.content.res.Resources;
 import android.database.ContentObserver;
-import android.graphics.Rect;
 import android.net.Uri;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -47,6 +46,8 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
+import com.android.systemui.plugins.ClockEvents;
+import com.android.systemui.plugins.ClockFaceController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.shared.clocks.ClockRegistry;
@@ -88,7 +89,15 @@
     @Mock
     KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     @Mock
-    private ClockController mClock;
+    private ClockController mClockController;
+    @Mock
+    private ClockFaceController mLargeClockController;
+    @Mock
+    private ClockFaceController mSmallClockController;
+    @Mock
+    private ClockAnimations mClockAnimations;
+    @Mock
+    private ClockEvents mClockEvents;
     @Mock
     DumpManager mDumpManager;
     @Mock
@@ -97,10 +106,12 @@
     @Mock
     private NotificationIconContainer mNotificationIcons;
     @Mock
-    private AnimatableClockView mClockView;
+    private AnimatableClockView mSmallClockView;
     @Mock
     private AnimatableClockView mLargeClockView;
     @Mock
+    private FrameLayout mSmallClockFrame;
+    @Mock
     private FrameLayout mLargeClockFrame;
     @Mock
     private SecureSettings mSecureSettings;
@@ -121,9 +132,14 @@
                 mock(RelativeLayout.LayoutParams.class));
         when(mView.getContext()).thenReturn(getContext());
         when(mView.getResources()).thenReturn(mResources);
+        when(mResources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin))
+                .thenReturn(100);
+        when(mResources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin))
+                .thenReturn(-200);
 
         when(mView.findViewById(R.id.lockscreen_clock_view_large)).thenReturn(mLargeClockFrame);
-        when(mClockView.getContext()).thenReturn(getContext());
+        when(mView.findViewById(R.id.lockscreen_clock_view)).thenReturn(mSmallClockFrame);
+        when(mSmallClockView.getContext()).thenReturn(getContext());
         when(mLargeClockView.getContext()).thenReturn(getContext());
 
         when(mView.isAttachedToWindow()).thenReturn(true);
@@ -144,7 +160,14 @@
         );
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
-        when(mClockRegistry.createCurrentClock()).thenReturn(mClock);
+        when(mLargeClockController.getView()).thenReturn(mLargeClockView);
+        when(mSmallClockController.getView()).thenReturn(mSmallClockView);
+        when(mClockController.getLargeClock()).thenReturn(mLargeClockController);
+        when(mClockController.getSmallClock()).thenReturn(mSmallClockController);
+        when(mClockController.getEvents()).thenReturn(mClockEvents);
+        when(mClockController.getAnimations()).thenReturn(mClockAnimations);
+        when(mClockRegistry.createCurrentClock()).thenReturn(mClockController);
+        when(mClockEventController.getClock()).thenReturn(mClockController);
 
         mSliceView = new View(getContext());
         when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView);
@@ -203,8 +226,8 @@
         verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture());
 
         listenerArgumentCaptor.getValue().onClockChanged();
-        verify(mView, times(2)).setClock(mClock, StatusBarState.SHADE);
-        verify(mClockEventController, times(2)).setClock(mClock);
+        verify(mView, times(2)).setClock(mClockController, StatusBarState.SHADE);
+        verify(mClockEventController, times(2)).setClock(mClockController);
     }
 
     @Test
@@ -262,17 +285,40 @@
 
     @Test
     public void testGetClockAnimationsForwardsToClock() {
-        ClockController mockClockController = mock(ClockController.class);
-        ClockAnimations mockClockAnimations = mock(ClockAnimations.class);
-        when(mClockEventController.getClock()).thenReturn(mockClockController);
-        when(mockClockController.getAnimations()).thenReturn(mockClockAnimations);
-
-        Rect r1 = new Rect(1, 2, 3, 4);
-        Rect r2 = new Rect(5, 6, 7, 8);
-        mController.getClockAnimations().onPositionUpdated(r1, r2, 0.2f);
-        verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.2f);
+        assertEquals(mClockAnimations, mController.getClockAnimations());
     }
 
+    @Test
+    public void testGetLargeClockBottom_returnsExpectedValue() {
+        when(mLargeClockFrame.getVisibility()).thenReturn(View.VISIBLE);
+        when(mLargeClockFrame.getHeight()).thenReturn(100);
+        when(mSmallClockFrame.getHeight()).thenReturn(50);
+        when(mLargeClockView.getHeight()).thenReturn(40);
+        when(mSmallClockView.getHeight()).thenReturn(20);
+        mController.init();
+
+        assertEquals(170, mController.getClockBottom(1000));
+    }
+
+    @Test
+    public void testGetSmallLargeClockBottom_returnsExpectedValue() {
+        when(mLargeClockFrame.getVisibility()).thenReturn(View.GONE);
+        when(mLargeClockFrame.getHeight()).thenReturn(100);
+        when(mSmallClockFrame.getHeight()).thenReturn(50);
+        when(mLargeClockView.getHeight()).thenReturn(40);
+        when(mSmallClockView.getHeight()).thenReturn(20);
+        mController.init();
+
+        assertEquals(1120, mController.getClockBottom(1000));
+    }
+
+    @Test
+    public void testGetClockBottom_nullClock_returnsZero() {
+        when(mClockEventController.getClock()).thenReturn(null);
+        assertEquals(0, mController.getClockBottom(10));
+    }
+
+
     private void verifyAttachment(VerificationMode times) {
         verify(mClockRegistry, times).registerClockChangeListener(
                 any(ClockRegistry.ClockChangeListener.class));
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index d20be56..d912793 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -30,64 +30,54 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class KeyguardPasswordViewControllerTest : SysuiTestCase() {
-    @Mock
-    private lateinit var keyguardPasswordView: KeyguardPasswordView
-    @Mock
-    private lateinit var passwordEntry: EditText
-    @Mock
-    lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-    @Mock
-    lateinit var securityMode: KeyguardSecurityModel.SecurityMode
-    @Mock
-    lateinit var lockPatternUtils: LockPatternUtils
-    @Mock
-    lateinit var keyguardSecurityCallback: KeyguardSecurityCallback
-    @Mock
-    lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory
-    @Mock
-    lateinit var latencyTracker: LatencyTracker
-    @Mock
-    lateinit var inputMethodManager: InputMethodManager
-    @Mock
-    lateinit var emergencyButtonController: EmergencyButtonController
-    @Mock
-    lateinit var mainExecutor: DelayableExecutor
-    @Mock
-    lateinit var falsingCollector: FalsingCollector
-    @Mock
-    lateinit var keyguardViewController: KeyguardViewController
-    @Mock
-    private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
-    @Mock
-    private lateinit var mKeyguardMessageAreaController:
-        KeyguardMessageAreaController<BouncerKeyguardMessageArea>
+  @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView
+  @Mock private lateinit var passwordEntry: EditText
+  @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+  @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode
+  @Mock lateinit var lockPatternUtils: LockPatternUtils
+  @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback
+  @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory
+  @Mock lateinit var latencyTracker: LatencyTracker
+  @Mock lateinit var inputMethodManager: InputMethodManager
+  @Mock lateinit var emergencyButtonController: EmergencyButtonController
+  @Mock lateinit var mainExecutor: DelayableExecutor
+  @Mock lateinit var falsingCollector: FalsingCollector
+  @Mock lateinit var keyguardViewController: KeyguardViewController
+  @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
+  @Mock
+  private lateinit var mKeyguardMessageAreaController:
+      KeyguardMessageAreaController<BouncerKeyguardMessageArea>
 
-    private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController
+  private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController
 
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        Mockito.`when`(
-            keyguardPasswordView
-                .requireViewById<BouncerKeyguardMessageArea>(R.id.bouncer_message_area)
-        ).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(
+  @Before
+  fun setup() {
+    MockitoAnnotations.initMocks(this)
+    Mockito.`when`(
+            keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>(
+                R.id.bouncer_message_area))
+        .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)
+    `when`(keyguardPasswordView.resources).thenReturn(context.resources)
+    keyguardPasswordViewController =
+        KeyguardPasswordViewController(
             keyguardPasswordView,
             keyguardUpdateMonitor,
             securityMode,
@@ -100,51 +90,48 @@
             mainExecutor,
             mContext.resources,
             falsingCollector,
-            keyguardViewController
-        )
-    }
+            keyguardViewController)
+  }
 
-    @Test
-    fun testFocusWhenBouncerIsShown() {
-        Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
-        Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
-        keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
-        keyguardPasswordView.post {
-            verify(keyguardPasswordView).requestFocus()
-            verify(keyguardPasswordView).showKeyboard()
-        }
+  @Test
+  fun testFocusWhenBouncerIsShown() {
+    Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
+    Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
+    keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
+    keyguardPasswordView.post {
+      verify(keyguardPasswordView).requestFocus()
+      verify(keyguardPasswordView).showKeyboard()
     }
+  }
 
-    @Test
-    fun testDoNotFocusWhenBouncerIsHidden() {
-        Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false)
-        Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
-        keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
-        verify(keyguardPasswordView, never()).requestFocus()
-    }
+  @Test
+  fun testDoNotFocusWhenBouncerIsHidden() {
+    Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false)
+    Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
+    keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
+    verify(keyguardPasswordView, never()).requestFocus()
+  }
 
-    @Test
-    fun testHideKeyboardWhenOnPause() {
-        keyguardPasswordViewController.onPause()
-        keyguardPasswordView.post {
-            verify(keyguardPasswordView).clearFocus()
-            verify(keyguardPasswordView).hideKeyboard()
-        }
+  @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)
-    }
+  @Test
+  fun startAppearAnimation() {
+    keyguardPasswordViewController.startAppearAnimation()
+    verify(mKeyguardMessageAreaController)
+        .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false)
+  }
 
-    @Test
-    fun startAppearAnimation_withExistingMessage() {
-        `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
-        keyguardPasswordViewController.startAppearAnimation()
-        verify(
-            mKeyguardMessageAreaController,
-            never()
-        ).setMessage(R.string.keyguard_enter_your_password)
-    }
+  @Test
+  fun startAppearAnimation_withExistingMessage() {
+    `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+    keyguardPasswordViewController.startAppearAnimation()
+    verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
+  }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index b3d1c8f..85dbdb8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -30,97 +30,93 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
-import org.mockito.Mockito.never
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class KeyguardPatternViewControllerTest : SysuiTestCase() {
-    @Mock
-    private lateinit var mKeyguardPatternView: KeyguardPatternView
+  @Mock private lateinit var mKeyguardPatternView: KeyguardPatternView
 
-    @Mock
-    private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor
+  @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor
 
-    @Mock
-    private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode
+  @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode
 
-    @Mock
-    private lateinit var mLockPatternUtils: LockPatternUtils
+  @Mock private lateinit var mLockPatternUtils: LockPatternUtils
 
-    @Mock
-    private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback
+  @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback
 
-    @Mock
-    private lateinit var mLatencyTracker: LatencyTracker
-    private var mFalsingCollector: FalsingCollector = FalsingCollectorFake()
+  @Mock private lateinit var mLatencyTracker: LatencyTracker
+  private var mFalsingCollector: FalsingCollector = FalsingCollectorFake()
 
-    @Mock
-    private lateinit var mEmergencyButtonController: EmergencyButtonController
+  @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController
 
-    @Mock
-    private lateinit
-    var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory
+  @Mock
+  private lateinit var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory
 
-    @Mock
-    private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
+  @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
 
-    @Mock
-    private lateinit var mKeyguardMessageAreaController:
-        KeyguardMessageAreaController<BouncerKeyguardMessageArea>
+  @Mock
+  private lateinit var mKeyguardMessageAreaController:
+      KeyguardMessageAreaController<BouncerKeyguardMessageArea>
 
-    @Mock
-    private lateinit var mLockPatternView: LockPatternView
+  @Mock private lateinit var mLockPatternView: LockPatternView
 
-    @Mock
-    private lateinit var mPostureController: DevicePostureController
+  @Mock private lateinit var mPostureController: DevicePostureController
 
-    private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
+  private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
 
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        `when`(mKeyguardPatternView.isAttachedToWindow).thenReturn(true)
-        `when`(mKeyguardPatternView
-            .requireViewById<BouncerKeyguardMessageArea>(R.id.bouncer_message_area))
-            .thenReturn(mKeyguardMessageArea)
-        `when`(mKeyguardPatternView.findViewById<LockPatternView>(R.id.lockPatternView))
-            .thenReturn(mLockPatternView)
-        `when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea))
-            .thenReturn(mKeyguardMessageAreaController)
-        mKeyguardPatternViewController = KeyguardPatternViewController(
+  @Before
+  fun setup() {
+    MockitoAnnotations.initMocks(this)
+    `when`(mKeyguardPatternView.isAttachedToWindow).thenReturn(true)
+    `when`(
+            mKeyguardPatternView.requireViewById<BouncerKeyguardMessageArea>(
+                R.id.bouncer_message_area))
+        .thenReturn(mKeyguardMessageArea)
+    `when`(mKeyguardPatternView.findViewById<LockPatternView>(R.id.lockPatternView))
+        .thenReturn(mLockPatternView)
+    `when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea))
+        .thenReturn(mKeyguardMessageAreaController)
+    `when`(mKeyguardPatternView.resources).thenReturn(context.resources)
+    mKeyguardPatternViewController =
+        KeyguardPatternViewController(
             mKeyguardPatternView,
-            mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
-            mLatencyTracker, mFalsingCollector, mEmergencyButtonController,
-            mKeyguardMessageAreaControllerFactory, mPostureController
-        )
-    }
+            mKeyguardUpdateMonitor,
+            mSecurityMode,
+            mLockPatternUtils,
+            mKeyguardSecurityCallback,
+            mLatencyTracker,
+            mFalsingCollector,
+            mEmergencyButtonController,
+            mKeyguardMessageAreaControllerFactory,
+            mPostureController)
+  }
 
-    @Test
-    fun onPause_resetsText() {
-        mKeyguardPatternViewController.init()
-        mKeyguardPatternViewController.onPause()
-        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
-    }
+  @Test
+  fun onPause_resetsText() {
+    mKeyguardPatternViewController.init()
+    mKeyguardPatternViewController.onPause()
+    verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
+  }
 
+  @Test
+  fun startAppearAnimation() {
+    mKeyguardPatternViewController.startAppearAnimation()
+    verify(mKeyguardMessageAreaController)
+        .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false)
+  }
 
-    @Test
-    fun startAppearAnimation() {
-        mKeyguardPatternViewController.startAppearAnimation()
-        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
-    }
-
-    @Test
-    fun startAppearAnimation_withExistingMessage() {
-        `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
-        mKeyguardPatternViewController.startAppearAnimation()
-        verify(
-            mKeyguardMessageAreaController,
-            never()
-        ).setMessage(R.string.keyguard_enter_your_password)
-    }
+  @Test
+  fun startAppearAnimation_withExistingMessage() {
+    `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+    mKeyguardPatternViewController.startAppearAnimation()
+    verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
+  }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index ce1101f..b742100 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.keyguard;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -113,4 +115,9 @@
         mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
         verify(mPasswordEntry).requestFocus();
     }
+
+    @Test
+    public void testGetInitialMessageResId() {
+        assertThat(mKeyguardPinViewController.getInitialMessageResId()).isNotEqualTo(0);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 8bcfe6f..cdb7bbb 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -31,10 +31,13 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.any
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -79,6 +82,7 @@
                 keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java))
             )
             .thenReturn(keyguardMessageAreaController)
+        `when`(keyguardPinView.resources).thenReturn(context.resources)
         pinViewController =
             KeyguardPinViewController(
                 keyguardPinView,
@@ -98,14 +102,14 @@
     @Test
     fun startAppearAnimation() {
         pinViewController.startAppearAnimation()
-        verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin)
+        verify(keyguardMessageAreaController)
+            .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
     }
 
     @Test
     fun startAppearAnimation_withExistingMessage() {
         Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
         pinViewController.startAppearAnimation()
-        verify(keyguardMessageAreaController, Mockito.never())
-            .setMessage(R.string.keyguard_enter_your_password)
+        verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean())
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index bdd29aa..0f4cf41 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -2083,6 +2083,96 @@
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
     }
 
+    @Test
+    public void fingerprintFailure_requestActiveUnlock_dismissKeyguard()
+            throws RemoteException {
+        // GIVEN shouldTriggerActiveUnlock
+        bouncerFullyVisible();
+        when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true);
+
+        // GIVEN active unlock triggers on biometric failures
+        when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                .thenReturn(true);
+
+        // WHEN fingerprint fails
+        mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback.onAuthenticationFailed();
+
+        // ALWAYS request unlock with a keyguard dismissal
+        verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()),
+                eq(true));
+    }
+
+    @Test
+    public void faceNonBypassFailure_requestActiveUnlock_doesNotDismissKeyguard()
+            throws RemoteException {
+        // GIVEN shouldTriggerActiveUnlock
+        when(mAuthController.isUdfpsFingerDown()).thenReturn(false);
+        keyguardIsVisible();
+        keyguardNotGoingAway();
+        statusBarShadeIsNotLocked();
+        when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true);
+
+        // GIVEN active unlock triggers on biometric failures
+        when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                .thenReturn(true);
+
+        // WHEN face fails & bypass is not allowed
+        lockscreenBypassIsNotAllowed();
+        mKeyguardUpdateMonitor.mFaceAuthenticationCallback.onAuthenticationFailed();
+
+        // THEN request unlock with NO keyguard dismissal
+        verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()),
+                eq(false));
+    }
+
+    @Test
+    public void faceBypassFailure_requestActiveUnlock_dismissKeyguard()
+            throws RemoteException {
+        // GIVEN shouldTriggerActiveUnlock
+        when(mAuthController.isUdfpsFingerDown()).thenReturn(false);
+        keyguardIsVisible();
+        keyguardNotGoingAway();
+        statusBarShadeIsNotLocked();
+        when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true);
+
+        // GIVEN active unlock triggers on biometric failures
+        when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                .thenReturn(true);
+
+        // WHEN face fails & bypass is not allowed
+        lockscreenBypassIsAllowed();
+        mKeyguardUpdateMonitor.mFaceAuthenticationCallback.onAuthenticationFailed();
+
+        // THEN request unlock with a keyguard dismissal
+        verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()),
+                eq(true));
+    }
+
+    @Test
+    public void faceNonBypassFailure_requestActiveUnlock_dismissKeyguard()
+            throws RemoteException {
+        // GIVEN shouldTriggerActiveUnlock
+        when(mAuthController.isUdfpsFingerDown()).thenReturn(false);
+        lockscreenBypassIsNotAllowed();
+        when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true);
+
+        // GIVEN active unlock triggers on biometric failures
+        when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+                .thenReturn(true);
+
+        // WHEN face fails & on the bouncer
+        bouncerFullyVisible();
+        mKeyguardUpdateMonitor.mFaceAuthenticationCallback.onAuthenticationFailed();
+
+        // THEN request unlock with a keyguard dismissal
+        verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()),
+                eq(true));
+    }
+
     private void userDeviceLockDown() {
         when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false);
         when(mStrongAuthTracker.getStrongAuthForUser(mCurrentUserId))
@@ -2100,6 +2190,9 @@
     }
 
     private void mockCanBypassLockscreen(boolean canBypass) {
+        // force update the isFaceEnrolled cache:
+        mKeyguardUpdateMonitor.isFaceAuthEnabledForUser(getCurrentUser());
+
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
         when(mKeyguardBypassController.canBypass()).thenReturn(canBypass);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
index ca94ea8..262b4b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
@@ -301,7 +301,7 @@
         val intent = intentCaptor.value
 
         assertThat(CameraIntents.isSecureCameraIntent(intent)).isEqualTo(isSecure)
-        assertThat(intent.getIntExtra(CameraGestureHelper.EXTRA_CAMERA_LAUNCH_SOURCE, -1))
+        assertThat(intent.getIntExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, -1))
             .isEqualTo(source)
     }
 
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 779788a..d172c9a 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
@@ -38,6 +38,7 @@
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.controller.StructureInfo
 import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.management.ControlsProviderSelectorActivity
 import com.android.systemui.controls.settings.FakeControlsSettingsRepository
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.ActivityStarter
@@ -53,6 +54,7 @@
 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.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.wm.shell.TaskView
 import com.android.wm.shell.TaskViewFactory
@@ -322,6 +324,45 @@
             .isFalse()
     }
 
+    @Test
+    fun testResolveActivityWhileSeeding_ControlsActivity() {
+        whenever(controlsController.addSeedingFavoritesCallback(any())).thenReturn(true)
+        assertThat(underTest.resolveActivity()).isEqualTo(ControlsActivity::class.java)
+    }
+
+    @Test
+    fun testResolveActivityNotSeedingNoFavoritesNoPanels_ControlsProviderSelectorActivity() {
+        whenever(controlsController.addSeedingFavoritesCallback(any())).thenReturn(false)
+        whenever(controlsController.getFavorites()).thenReturn(emptyList())
+
+        val selectedItems =
+            listOf(
+                SelectedItem.StructureItem(
+                    StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList())
+                ),
+            )
+        sharedPreferences
+            .edit()
+            .putString("controls_component", selectedItems[0].componentName.flattenToString())
+            .putString("controls_structure", selectedItems[0].name.toString())
+            .commit()
+
+        assertThat(underTest.resolveActivity())
+            .isEqualTo(ControlsProviderSelectorActivity::class.java)
+    }
+
+    @Test
+    fun testResolveActivityNotSeedingNoDefaultNoFavoritesPanel_ControlsActivity() {
+        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        val activity = ComponentName("pkg", "activity")
+        val csi = ControlsServiceInfo(panel.componentName, panel.appName, activity)
+        whenever(controlsController.addSeedingFavoritesCallback(any())).thenReturn(true)
+        whenever(controlsController.getFavorites()).thenReturn(emptyList())
+        whenever(controlsListingController.getCurrentServices()).thenReturn(listOf(csi))
+
+        assertThat(underTest.resolveActivity()).isEqualTo(ControlsActivity::class.java)
+    }
+
     private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo {
         val activity = ComponentName("pkg", "activity")
         sharedPreferences
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt
index 5cd2ace..de04ef8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt
@@ -75,6 +75,7 @@
             uiExecutor.execute(it.arguments[0] as Runnable)
             true
         }
+        whenever(activityContext.resources).thenReturn(context.resources)
 
         uiExecutor = FakeExecutor(FakeSystemClock())
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
index 99406ed..8e689cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
@@ -23,11 +23,11 @@
 class DreamOverlayAnimationsControllerTest : SysuiTestCase() {
 
     companion object {
+        private const val DREAM_BLUR_RADIUS = 50
         private const val DREAM_IN_BLUR_ANIMATION_DURATION = 1L
-        private const val DREAM_IN_BLUR_ANIMATION_DELAY = 2L
         private const val DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = 3L
-        private const val DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY = 4L
-        private const val DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY = 5L
+        private const val DREAM_IN_TRANSLATION_Y_DISTANCE = 6
+        private const val DREAM_IN_TRANSLATION_Y_DURATION = 7L
         private const val DREAM_OUT_TRANSLATION_Y_DISTANCE = 6
         private const val DREAM_OUT_TRANSLATION_Y_DURATION = 7L
         private const val DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM = 8L
@@ -54,11 +54,11 @@
                 hostViewController,
                 statusBarViewController,
                 stateController,
+                DREAM_BLUR_RADIUS,
                 DREAM_IN_BLUR_ANIMATION_DURATION,
-                DREAM_IN_BLUR_ANIMATION_DELAY,
                 DREAM_IN_COMPLICATIONS_ANIMATION_DURATION,
-                DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY,
-                DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY,
+                DREAM_IN_TRANSLATION_Y_DISTANCE,
+                DREAM_IN_TRANSLATION_Y_DURATION,
                 DREAM_OUT_TRANSLATION_Y_DISTANCE,
                 DREAM_OUT_TRANSLATION_Y_DURATION,
                 DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 99ca9a6..430575c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -19,7 +19,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -39,16 +38,18 @@
 import android.view.WindowManagerImpl;
 
 import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleRegistry;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.complication.ComplicationLayoutEngine;
 import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.dreams.dreamcomplication.HideComplicationTouchHandler;
 import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
+import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
@@ -72,7 +73,7 @@
     private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
 
     @Mock
-    LifecycleOwner mLifecycleOwner;
+    DreamOverlayLifecycleOwner mLifecycleOwner;
 
     @Mock
     LifecycleRegistry mLifecycleRegistry;
@@ -95,6 +96,20 @@
     ComplicationComponent mComplicationComponent;
 
     @Mock
+    ComplicationLayoutEngine mComplicationVisibilityController;
+
+    @Mock
+    com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent.Factory
+            mDreamComplicationComponentFactory;
+
+    @Mock
+    com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent
+            mDreamComplicationComponent;
+
+    @Mock
+    HideComplicationTouchHandler mHideComplicationTouchHandler;
+
+    @Mock
     DreamOverlayComponent.Factory mDreamOverlayComponentFactory;
 
     @Mock
@@ -119,6 +134,9 @@
     ViewGroup mDreamOverlayContainerViewParent;
 
     @Mock
+    TouchInsetManager mTouchInsetManager;
+
+    @Mock
     UiEventLogger mUiEventLogger;
 
     @Captor
@@ -132,29 +150,38 @@
 
         when(mDreamOverlayComponent.getDreamOverlayContainerViewController())
                 .thenReturn(mDreamOverlayContainerViewController);
-        when(mDreamOverlayComponent.getLifecycleOwner())
-                .thenReturn(mLifecycleOwner);
-        when(mDreamOverlayComponent.getLifecycleRegistry())
+        when(mLifecycleOwner.getRegistry())
                 .thenReturn(mLifecycleRegistry);
         when(mDreamOverlayComponent.getDreamOverlayTouchMonitor())
                 .thenReturn(mDreamOverlayTouchMonitor);
         when(mComplicationComponentFactory
-                .create())
+                .create(any(), any(), any(), any()))
                 .thenReturn(mComplicationComponent);
-        // TODO(b/261781069): A touch handler should be passed in from the complication component
-        // when the complication component is introduced.
+        when(mComplicationComponent.getVisibilityController())
+                .thenReturn(mComplicationVisibilityController);
+        when(mDreamComplicationComponent.getHideComplicationTouchHandler())
+                .thenReturn(mHideComplicationTouchHandler);
+        when(mDreamComplicationComponentFactory
+                .create(any(), any()))
+                .thenReturn(mDreamComplicationComponent);
         when(mDreamOverlayComponentFactory
-                .create(any(), any(), isNull()))
+                .create(any(), any(), any(), any()))
                 .thenReturn(mDreamOverlayComponent);
         when(mDreamOverlayContainerViewController.getContainerView())
                 .thenReturn(mDreamOverlayContainerView);
 
-        mService = new DreamOverlayService(mContext, mMainExecutor, mWindowManager,
+        mService = new DreamOverlayService(
+                mContext,
+                mMainExecutor,
+                mLifecycleOwner,
+                mWindowManager,
                 mComplicationComponentFactory,
+                mDreamComplicationComponentFactory,
                 mDreamOverlayComponentFactory,
                 mStateController,
                 mKeyguardUpdateMonitor,
                 mUiEventLogger,
+                mTouchInsetManager,
                 LOW_LIGHT_COMPONENT);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index c21c7a2..ee989d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -63,7 +63,7 @@
     @Test
     public void testStateChange_overlayActive() {
         final DreamOverlayStateController stateController = new DreamOverlayStateController(
-                mExecutor);
+                mExecutor, true);
         stateController.addCallback(mCallback);
         stateController.setOverlayActive(true);
         mExecutor.runAllReady();
@@ -85,7 +85,7 @@
     @Test
     public void testCallback() {
         final DreamOverlayStateController stateController = new DreamOverlayStateController(
-                mExecutor);
+                mExecutor, true);
         stateController.addCallback(mCallback);
 
         // Add complication and verify callback is notified.
@@ -111,7 +111,7 @@
     @Test
     public void testNotifyOnCallbackAdd() {
         final DreamOverlayStateController stateController =
-                new DreamOverlayStateController(mExecutor);
+                new DreamOverlayStateController(mExecutor, true);
 
         stateController.addComplication(mComplication);
         mExecutor.runAllReady();
@@ -123,9 +123,24 @@
     }
 
     @Test
+    public void testNotifyOnCallbackAddOverlayDisabled() {
+        final DreamOverlayStateController stateController =
+                new DreamOverlayStateController(mExecutor, false);
+
+        stateController.addComplication(mComplication);
+        mExecutor.runAllReady();
+
+        // Verify callback occurs on add when an overlay is already present.
+        stateController.addCallback(mCallback);
+        mExecutor.runAllReady();
+        verify(mCallback, never()).onComplicationsChanged();
+    }
+
+
+    @Test
     public void testComplicationFilteringWhenShouldShowComplications() {
         final DreamOverlayStateController stateController =
-                new DreamOverlayStateController(mExecutor);
+                new DreamOverlayStateController(mExecutor, true);
         stateController.setShouldShowComplications(true);
 
         final Complication alwaysAvailableComplication = Mockito.mock(Complication.class);
@@ -165,7 +180,7 @@
     @Test
     public void testComplicationFilteringWhenShouldHideComplications() {
         final DreamOverlayStateController stateController =
-                new DreamOverlayStateController(mExecutor);
+                new DreamOverlayStateController(mExecutor, true);
         stateController.setShouldShowComplications(true);
 
         final Complication alwaysAvailableComplication = Mockito.mock(Complication.class);
@@ -212,7 +227,7 @@
     public void testComplicationWithNoTypeNotFiltered() {
         final Complication complication = Mockito.mock(Complication.class);
         final DreamOverlayStateController stateController =
-                new DreamOverlayStateController(mExecutor);
+                new DreamOverlayStateController(mExecutor, true);
         stateController.addComplication(complication);
         mExecutor.runAllReady();
         assertThat(stateController.getComplications(true).contains(complication))
@@ -222,7 +237,7 @@
     @Test
     public void testNotifyLowLightChanged() {
         final DreamOverlayStateController stateController =
-                new DreamOverlayStateController(mExecutor);
+                new DreamOverlayStateController(mExecutor, true);
 
         stateController.addCallback(mCallback);
         mExecutor.runAllReady();
@@ -238,7 +253,7 @@
     @Test
     public void testNotifyEntryAnimationsFinishedChanged() {
         final DreamOverlayStateController stateController =
-                new DreamOverlayStateController(mExecutor);
+                new DreamOverlayStateController(mExecutor, true);
 
         stateController.addCallback(mCallback);
         mExecutor.runAllReady();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
index b477592..dcd8736 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.dreams.complication;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -139,6 +141,21 @@
     }
 
     @Test
+    public void testMalformedComplicationAddition() {
+        final Observer<Collection<ComplicationViewModel>> observer =
+                captureComplicationViewModelsObserver();
+
+        // Add a complication and ensure it is added to the view.
+        final HashSet<ComplicationViewModel> complications = new HashSet<>(
+                Collections.singletonList(mComplicationViewModel));
+        when(mViewHolder.getView()).thenReturn(null);
+        observer.onChanged(complications);
+
+        verify(mLayoutEngine, never()).addComplication(any(), any(), any(), anyInt());
+
+    }
+
+    @Test
     public void testNewComplicationsBeforeEntryAnimationsFinishSetToInvisible() {
         final Observer<Collection<ComplicationViewModel>> observer =
                 captureComplicationViewModelsObserver();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
index fdb4cc4..e414942 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
@@ -17,6 +17,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
@@ -29,6 +33,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.function.Consumer;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -197,4 +202,19 @@
         assertThat(paramsWithConstraint.constraintSpecified()).isTrue();
         assertThat(paramsWithConstraint.getConstraint()).isEqualTo(constraint);
     }
+
+    @Test
+    public void testIteratePositions() {
+        final int positions = ComplicationLayoutParams.POSITION_TOP
+                | ComplicationLayoutParams.POSITION_START
+                | ComplicationLayoutParams.POSITION_END;
+        final Consumer<Integer> consumer = mock(Consumer.class);
+
+        ComplicationLayoutParams.iteratePositions(consumer, positions);
+
+        verify(consumer).accept(ComplicationLayoutParams.POSITION_TOP);
+        verify(consumer).accept(ComplicationLayoutParams.POSITION_START);
+        verify(consumer).accept(ComplicationLayoutParams.POSITION_END);
+        verify(consumer, never()).accept(ComplicationLayoutParams.POSITION_BOTTOM);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index e6d3a69..89c7280 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.testing.AndroidTestingRunner;
 import android.view.View;
@@ -54,6 +55,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 
@@ -147,6 +149,19 @@
     }
 
     @Test
+    public void complicationAvailability_serviceAvailable_noFavorites_panel_addComplication() {
+        final DreamHomeControlsComplication.Registrant registrant =
+                new DreamHomeControlsComplication.Registrant(mComplication,
+                        mDreamOverlayStateController, mControlsComponent);
+        registrant.start();
+
+        setHaveFavorites(false);
+        setServiceWithPanel();
+
+        verify(mDreamOverlayStateController).addComplication(mComplication);
+    }
+
+    @Test
     public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
@@ -232,6 +247,15 @@
         triggerControlsListingCallback(serviceInfos);
     }
 
+    private void setServiceWithPanel() {
+        final List<ControlsServiceInfo> serviceInfos = new ArrayList<>();
+        ControlsServiceInfo csi = mock(ControlsServiceInfo.class);
+        serviceInfos.add(csi);
+        when(csi.getPanelActivity()).thenReturn(new ComponentName("a", "b"));
+        when(mControlsListingController.getCurrentServices()).thenReturn(serviceInfos);
+        triggerControlsListingCallback(serviceInfos);
+    }
+
     private void setDreamOverlayActive(boolean value) {
         when(mDreamOverlayStateController.isOverlayActive()).thenReturn(value);
         verify(mDreamOverlayStateController).addCallback(mStateCallbackCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandlerTest.java
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandlerTest.java
index 4e3aca7..d68f032 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandlerTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.dreams.touch;
+package com.android.systemui.dreams.dreamcomplication;
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -33,6 +33,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.Complication;
+import com.android.systemui.dreams.touch.DreamTouchHandler;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.touch.TouchInsetManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
index cef452b..09c8e6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
@@ -20,7 +20,13 @@
 import android.content.ContentValues
 import android.content.pm.PackageManager
 import android.content.pm.ProviderInfo
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
 import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.SurfaceControlViewHost
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SystemUIAppComponentFactoryBase
@@ -36,6 +42,9 @@
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
+import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRendererFactory
+import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
@@ -43,40 +52,53 @@
 import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.mockito.any
 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.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var previewRendererFactory: KeyguardPreviewRendererFactory
+    @Mock private lateinit var previewRenderer: KeyguardPreviewRenderer
+    @Mock private lateinit var backgroundHandler: Handler
+    @Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage
 
     private lateinit var underTest: KeyguardQuickAffordanceProvider
 
+    private lateinit var testScope: TestScope
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        whenever(previewRenderer.surfacePackage).thenReturn(previewSurfacePackage)
+        whenever(previewRendererFactory.create(any())).thenReturn(previewRenderer)
+        whenever(backgroundHandler.looper).thenReturn(TestableLooper.get(this).looper)
 
         underTest = KeyguardQuickAffordanceProvider()
-        val scope = CoroutineScope(IMMEDIATE)
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
         val localUserSelectionManager =
             KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
@@ -96,7 +118,7 @@
             )
         val remoteUserSelectionManager =
             KeyguardQuickAffordanceRemoteUserSelectionManager(
-                scope = scope,
+                scope = testScope.backgroundScope,
                 userTracker = userTracker,
                 clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
                 userHandle = UserHandle.SYSTEM,
@@ -104,7 +126,7 @@
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
-                scope = scope,
+                scope = testScope.backgroundScope,
                 localUserSelectionManager = localUserSelectionManager,
                 remoteUserSelectionManager = remoteUserSelectionManager,
                 userTracker = userTracker,
@@ -123,8 +145,8 @@
                     ),
                 legacySettingSyncer =
                     KeyguardQuickAffordanceLegacySettingSyncer(
-                        scope = scope,
-                        backgroundDispatcher = IMMEDIATE,
+                        scope = testScope.backgroundScope,
+                        backgroundDispatcher = testDispatcher,
                         secureSettings = FakeSettings(),
                         selectionsManager = localUserSelectionManager,
                     ),
@@ -148,6 +170,12 @@
                     },
                 repository = { quickAffordanceRepository },
             )
+        underTest.previewManager =
+            KeyguardRemotePreviewManager(
+                previewRendererFactory = previewRendererFactory,
+                mainDispatcher = testDispatcher,
+                backgroundHandler = backgroundHandler,
+            )
 
         underTest.attachInfoForTesting(
             context,
@@ -190,7 +218,7 @@
 
     @Test
     fun `insert and query selection`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
             val affordanceId = AFFORDANCE_2
             val affordanceName = AFFORDANCE_2_NAME
@@ -214,7 +242,7 @@
 
     @Test
     fun `query slots`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             assertThat(querySlots())
                 .isEqualTo(
                     listOf(
@@ -232,7 +260,7 @@
 
     @Test
     fun `query affordances`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             assertThat(queryAffordances())
                 .isEqualTo(
                     listOf(
@@ -252,7 +280,7 @@
 
     @Test
     fun `delete and query selection`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             insertSelection(
                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                 affordanceId = AFFORDANCE_1,
@@ -286,7 +314,7 @@
 
     @Test
     fun `delete all selections in a slot`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             insertSelection(
                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                 affordanceId = AFFORDANCE_1,
@@ -316,6 +344,23 @@
                 )
         }
 
+    @Test
+    fun preview() =
+        testScope.runTest {
+            val hostToken: IBinder = mock()
+            whenever(previewRenderer.hostToken).thenReturn(hostToken)
+            val extras = Bundle()
+
+            val result = underTest.call("whatever", "anything", extras)
+
+            verify(previewRenderer).render()
+            verify(hostToken).linkToDeath(any(), anyInt())
+            assertThat(result!!).isNotNull()
+            assertThat(result.get(KeyguardRemotePreviewManager.KEY_PREVIEW_SURFACE_PACKAGE))
+                .isEqualTo(previewSurfacePackage)
+            assertThat(result.containsKey(KeyguardRemotePreviewManager.KEY_PREVIEW_CALLBACK))
+        }
+
     private fun insertSelection(
         slotId: String,
         affordanceId: String,
@@ -451,7 +496,6 @@
     )
 
     companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
         private const val AFFORDANCE_1 = "affordance_1"
         private const val AFFORDANCE_2 = "affordance_2"
         private const val AFFORDANCE_1_NAME = "affordance_1_name"
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 798839d..804960d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -168,6 +168,45 @@
     }
 
     @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void testOnStartedWakingUp_whileSleeping_ifWakeAndUnlocking_doesNotShowKeyguard() {
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
+        when(mLockPatternUtils.getPowerButtonInstantlyLocks(anyInt())).thenReturn(true);
+        mViewMediator.onSystemReady();
+        TestableLooper.get(this).processAllMessages();
+
+        mViewMediator.setShowingLocked(false);
+        TestableLooper.get(this).processAllMessages();
+
+        mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER);
+        mViewMediator.onWakeAndUnlocking();
+        mViewMediator.onStartedWakingUp(OFF_BECAUSE_OF_USER, false);
+        TestableLooper.get(this).processAllMessages();
+
+        assertFalse(mViewMediator.isShowingAndNotOccluded());
+        verify(mKeyguardStateController, never()).notifyKeyguardState(eq(true), anyBoolean());
+    }
+
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void testOnStartedWakingUp_whileSleeping_ifNotWakeAndUnlocking_showsKeyguard() {
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
+        when(mLockPatternUtils.getPowerButtonInstantlyLocks(anyInt())).thenReturn(true);
+        mViewMediator.onSystemReady();
+        TestableLooper.get(this).processAllMessages();
+
+        mViewMediator.setShowingLocked(false);
+        TestableLooper.get(this).processAllMessages();
+
+        mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER);
+        mViewMediator.onStartedWakingUp(OFF_BECAUSE_OF_USER, false);
+
+        TestableLooper.get(this).processAllMessages();
+
+        assertTrue(mViewMediator.isShowingAndNotOccluded());
+    }
+
+    @Test
     public void testRegisterDumpable() {
         verify(mDumpManager).registerDumpable(KeyguardViewMediator.class.getName(), mViewMediator);
         verify(mStatusBarKeyguardViewManager, never()).setKeyguardGoingAwayState(anyBoolean());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index 322014a..f8cb408 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -20,13 +20,14 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.dagger.ControlsComponent
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import java.util.*
+import java.util.Optional
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -50,20 +51,22 @@
     companion object {
         @Parameters(
             name =
-                "feature enabled = {0}, has favorites = {1}, has service infos = {2}, can show" +
-                    " while locked = {3}, visibility is AVAILABLE {4} - expected visible = {5}"
+                "feature enabled = {0}, has favorites = {1}, has panels = {2}, " +
+                    "has service infos = {3}, can show while locked = {4}, " +
+                    "visibility is AVAILABLE {5} - expected visible = {6}"
         )
         @JvmStatic
         fun data() =
-            (0 until 32)
+            (0 until 64)
                 .map { combination ->
                     arrayOf(
-                        /* isFeatureEnabled= */ combination and 0b10000 != 0,
-                        /* hasFavorites= */ combination and 0b01000 != 0,
-                        /* hasServiceInfos= */ combination and 0b00100 != 0,
-                        /* canShowWhileLocked= */ combination and 0b00010 != 0,
-                        /* visibilityAvailable= */ combination and 0b00001 != 0,
-                        /* isVisible= */ combination == 0b11111,
+                        /* isFeatureEnabled= */ combination and 0b100000 != 0,
+                        /* hasFavorites = */ combination and 0b010000 != 0,
+                        /* hasPanels = */ combination and 0b001000 != 0,
+                        /* hasServiceInfos= */ combination and 0b000100 != 0,
+                        /* canShowWhileLocked= */ combination and 0b000010 != 0,
+                        /* visibilityAvailable= */ combination and 0b000001 != 0,
+                        /* isVisible= */ combination in setOf(0b111111, 0b110111, 0b101111),
                     )
                 }
                 .toList()
@@ -72,6 +75,7 @@
     @Mock private lateinit var component: ControlsComponent
     @Mock private lateinit var controlsController: ControlsController
     @Mock private lateinit var controlsListingController: ControlsListingController
+    @Mock private lateinit var controlsServiceInfo: ControlsServiceInfo
     @Captor
     private lateinit var callbackCaptor:
         ArgumentCaptor<ControlsListingController.ControlsListingCallback>
@@ -80,10 +84,11 @@
 
     @JvmField @Parameter(0) var isFeatureEnabled: Boolean = false
     @JvmField @Parameter(1) var hasFavorites: Boolean = false
-    @JvmField @Parameter(2) var hasServiceInfos: Boolean = false
-    @JvmField @Parameter(3) var canShowWhileLocked: Boolean = false
-    @JvmField @Parameter(4) var isVisibilityAvailable: Boolean = false
-    @JvmField @Parameter(5) var isVisibleExpected: Boolean = false
+    @JvmField @Parameter(2) var hasPanels: Boolean = false
+    @JvmField @Parameter(3) var hasServiceInfos: Boolean = false
+    @JvmField @Parameter(4) var canShowWhileLocked: Boolean = false
+    @JvmField @Parameter(5) var isVisibilityAvailable: Boolean = false
+    @JvmField @Parameter(6) var isVisibleExpected: Boolean = false
 
     @Before
     fun setUp() {
@@ -93,10 +98,13 @@
         whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
         whenever(component.getControlsListingController())
             .thenReturn(Optional.of(controlsListingController))
+        if (hasPanels) {
+            whenever(controlsServiceInfo.panelActivity).thenReturn(mock())
+        }
         whenever(controlsListingController.getCurrentServices())
             .thenReturn(
                 if (hasServiceInfos) {
-                    listOf(mock(), mock())
+                    listOf(controlsServiceInfo, mock())
                 } else {
                     emptyList()
                 }
@@ -134,10 +142,15 @@
         val job = underTest.lockScreenState.onEach(values::add).launchIn(this)
 
         if (canShowWhileLocked) {
+            val serviceInfoMock: ControlsServiceInfo = mock {
+                if (hasPanels) {
+                    whenever(panelActivity).thenReturn(mock())
+                }
+            }
             verify(controlsListingController).addCallback(callbackCaptor.capture())
             callbackCaptor.value.onServicesUpdated(
                 if (hasServiceInfos) {
-                    listOf(mock())
+                    listOf(serviceInfoMock)
                 } else {
                     emptyList()
                 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 11fe905..d97571bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
@@ -49,14 +50,10 @@
 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.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.runBlockingTest
-import kotlinx.coroutines.yield
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -78,6 +75,7 @@
 
     private lateinit var underTest: KeyguardQuickAffordanceInteractor
 
+    private lateinit var testScope: TestScope
     private lateinit var repository: FakeKeyguardRepository
     private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig
     private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig
@@ -99,7 +97,8 @@
             )
         qrCodeScanner =
             FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
-        val scope = CoroutineScope(IMMEDIATE)
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
 
         val localUserSelectionManager =
             KeyguardQuickAffordanceLocalUserSelectionManager(
@@ -120,7 +119,7 @@
             )
         val remoteUserSelectionManager =
             KeyguardQuickAffordanceRemoteUserSelectionManager(
-                scope = scope,
+                scope = testScope.backgroundScope,
                 userTracker = userTracker,
                 clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
                 userHandle = UserHandle.SYSTEM,
@@ -128,14 +127,14 @@
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
-                scope = scope,
+                scope = testScope.backgroundScope,
                 localUserSelectionManager = localUserSelectionManager,
                 remoteUserSelectionManager = remoteUserSelectionManager,
                 userTracker = userTracker,
                 legacySettingSyncer =
                     KeyguardQuickAffordanceLegacySettingSyncer(
-                        scope = scope,
-                        backgroundDispatcher = IMMEDIATE,
+                        scope = testScope.backgroundScope,
+                        backgroundDispatcher = testDispatcher,
                         secureSettings = FakeSettings(),
                         selectionsManager = localUserSelectionManager,
                     ),
@@ -175,88 +174,76 @@
     }
 
     @Test
-    fun `quickAffordance - bottom start affordance is visible`() = runBlockingTest {
-        val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
-        homeControls.setState(
-            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                icon = ICON,
-                activationState = ActivationState.Active,
+    fun `quickAffordance - bottom start affordance is visible`() =
+        testScope.runTest {
+            val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    icon = ICON,
+                    activationState = ActivationState.Active,
+                )
             )
-        )
 
-        var latest: KeyguardQuickAffordanceModel? = null
-        val job =
-            underTest
-                .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
-                .onEach { latest = it }
-                .launchIn(this)
-        // The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't
-        // produce an initial value. We yield to give the coroutine time to emit the first real
-        // value from our config.
-        yield()
+            val collectedValue =
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                )
 
-        assertThat(latest).isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
-        val visibleModel = latest as KeyguardQuickAffordanceModel.Visible
-        assertThat(visibleModel.configKey).isEqualTo(configKey)
-        assertThat(visibleModel.icon).isEqualTo(ICON)
-        assertThat(visibleModel.icon.contentDescription)
-            .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
-        assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active)
-        job.cancel()
-    }
+            assertThat(collectedValue())
+                .isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
+            val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible
+            assertThat(visibleModel.configKey).isEqualTo(configKey)
+            assertThat(visibleModel.icon).isEqualTo(ICON)
+            assertThat(visibleModel.icon.contentDescription)
+                .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
+            assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active)
+        }
 
     @Test
-    fun `quickAffordance - bottom end affordance is visible`() = runBlockingTest {
-        val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
-        quickAccessWallet.setState(
-            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                icon = ICON,
+    fun `quickAffordance - bottom end affordance is visible`() =
+        testScope.runTest {
+            val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    icon = ICON,
+                )
             )
-        )
 
-        var latest: KeyguardQuickAffordanceModel? = null
-        val job =
-            underTest
-                .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
-                .onEach { latest = it }
-                .launchIn(this)
-        // The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't
-        // produce an initial value. We yield to give the coroutine time to emit the first real
-        // value from our config.
-        yield()
+            val collectedValue =
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                )
 
-        assertThat(latest).isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
-        val visibleModel = latest as KeyguardQuickAffordanceModel.Visible
-        assertThat(visibleModel.configKey).isEqualTo(configKey)
-        assertThat(visibleModel.icon).isEqualTo(ICON)
-        assertThat(visibleModel.icon.contentDescription)
-            .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
-        assertThat(visibleModel.activationState).isEqualTo(ActivationState.NotSupported)
-        job.cancel()
-    }
+            assertThat(collectedValue())
+                .isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
+            val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible
+            assertThat(visibleModel.configKey).isEqualTo(configKey)
+            assertThat(visibleModel.icon).isEqualTo(ICON)
+            assertThat(visibleModel.icon.contentDescription)
+                .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
+            assertThat(visibleModel.activationState).isEqualTo(ActivationState.NotSupported)
+        }
 
     @Test
-    fun `quickAffordance - bottom start affordance hidden while dozing`() = runBlockingTest {
-        repository.setDozing(true)
-        homeControls.setState(
-            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                icon = ICON,
+    fun `quickAffordance - bottom start affordance hidden while dozing`() =
+        testScope.runTest {
+            repository.setDozing(true)
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    icon = ICON,
+                )
             )
-        )
 
-        var latest: KeyguardQuickAffordanceModel? = null
-        val job =
-            underTest
-                .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
-                .onEach { latest = it }
-                .launchIn(this)
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
-        job.cancel()
-    }
+            val collectedValue =
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                )
+            assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
+        }
 
     @Test
     fun `quickAffordance - bottom start affordance hidden when lockscreen is not showing`() =
-        runBlockingTest {
+        testScope.runTest {
             repository.setKeyguardShowing(false)
             homeControls.setState(
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(
@@ -264,19 +251,45 @@
                 )
             )
 
-            var latest: KeyguardQuickAffordanceModel? = null
-            val job =
-                underTest
-                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
-                    .onEach { latest = it }
-                    .launchIn(this)
-            assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
-            job.cancel()
+            val collectedValue =
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                )
+            assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
+        }
+
+    @Test
+    fun `quickAffordanceAlwaysVisible - even when lock screen not showing and dozing`() =
+        testScope.runTest {
+            repository.setKeyguardShowing(false)
+            repository.setDozing(true)
+            val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    icon = ICON,
+                    activationState = ActivationState.Active,
+                )
+            )
+
+            val collectedValue =
+                collectLastValue(
+                    underTest.quickAffordanceAlwaysVisible(
+                        KeyguardQuickAffordancePosition.BOTTOM_START
+                    )
+                )
+            assertThat(collectedValue())
+                .isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
+            val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible
+            assertThat(visibleModel.configKey).isEqualTo(configKey)
+            assertThat(visibleModel.icon).isEqualTo(ICON)
+            assertThat(visibleModel.icon.contentDescription)
+                .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
+            assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active)
         }
 
     @Test
     fun select() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
             homeControls.setState(
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
@@ -296,23 +309,18 @@
                     )
                 )
 
-            var startConfig: KeyguardQuickAffordanceModel? = null
-            val job1 =
-                underTest
-                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
-                    .onEach { startConfig = it }
-                    .launchIn(this)
-            var endConfig: KeyguardQuickAffordanceModel? = null
-            val job2 =
-                underTest
-                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
-                    .onEach { endConfig = it }
-                    .launchIn(this)
+            val startConfig =
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                )
+            val endConfig =
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                )
 
             underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
-            yield()
-            yield()
-            assertThat(startConfig)
+
+            assertThat(startConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Visible(
                         configKey =
@@ -322,7 +330,7 @@
                         activationState = ActivationState.NotSupported,
                     )
                 )
-            assertThat(endConfig)
+            assertThat(endConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Hidden,
                 )
@@ -345,9 +353,8 @@
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                 quickAccessWallet.key
             )
-            yield()
-            yield()
-            assertThat(startConfig)
+
+            assertThat(startConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Visible(
                         configKey =
@@ -357,7 +364,7 @@
                         activationState = ActivationState.NotSupported,
                     )
                 )
-            assertThat(endConfig)
+            assertThat(endConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Hidden,
                 )
@@ -377,9 +384,8 @@
                 )
 
             underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, qrCodeScanner.key)
-            yield()
-            yield()
-            assertThat(startConfig)
+
+            assertThat(startConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Visible(
                         configKey =
@@ -389,7 +395,7 @@
                         activationState = ActivationState.NotSupported,
                     )
                 )
-            assertThat(endConfig)
+            assertThat(endConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Visible(
                         configKey =
@@ -420,14 +426,11 @@
                             ),
                     )
                 )
-
-            job1.cancel()
-            job2.cancel()
         }
 
     @Test
     fun `unselect - one`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
             homeControls.setState(
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
@@ -439,34 +442,23 @@
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
             )
 
-            var startConfig: KeyguardQuickAffordanceModel? = null
-            val job1 =
-                underTest
-                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
-                    .onEach { startConfig = it }
-                    .launchIn(this)
-            var endConfig: KeyguardQuickAffordanceModel? = null
-            val job2 =
-                underTest
-                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
-                    .onEach { endConfig = it }
-                    .launchIn(this)
+            val startConfig =
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                )
+            val endConfig =
+                collectLastValue(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                )
             underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
-            yield()
-            yield()
             underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
-            yield()
-            yield()
-
             underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
-            yield()
-            yield()
 
-            assertThat(startConfig)
+            assertThat(startConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Hidden,
                 )
-            assertThat(endConfig)
+            assertThat(endConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Visible(
                         configKey =
@@ -495,14 +487,12 @@
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
                 quickAccessWallet.key
             )
-            yield()
-            yield()
 
-            assertThat(startConfig)
+            assertThat(startConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Hidden,
                 )
-            assertThat(endConfig)
+            assertThat(endConfig())
                 .isEqualTo(
                     KeyguardQuickAffordanceModel.Hidden,
                 )
@@ -513,14 +503,11 @@
                         KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
                     )
                 )
-
-            job1.cancel()
-            job2.cancel()
         }
 
     @Test
     fun `unselect - all`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
             homeControls.setState(
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
@@ -533,15 +520,8 @@
             )
 
             underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
-            yield()
-            yield()
             underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
-            yield()
-            yield()
-
             underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, null)
-            yield()
-            yield()
 
             assertThat(underTest.getSelections())
                 .isEqualTo(
@@ -562,8 +542,6 @@
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
                 null,
             )
-            yield()
-            yield()
 
             assertThat(underTest.getSelections())
                 .isEqualTo(
@@ -584,6 +562,5 @@
                 )
         }
         private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
-        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 83a5d0e..0abff88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.doze.util.BurnInHelperWrapper
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
@@ -44,20 +45,21 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.any
 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 kotlin.math.max
 import kotlin.math.min
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.runBlockingTest
-import kotlinx.coroutines.yield
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -67,9 +69,9 @@
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
@@ -83,6 +85,7 @@
 
     private lateinit var underTest: KeyguardBottomAreaViewModel
 
+    private lateinit var testScope: TestScope
     private lateinit var repository: FakeKeyguardRepository
     private lateinit var registry: FakeKeyguardQuickAffordanceRegistry
     private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig
@@ -123,7 +126,8 @@
         whenever(userTracker.userHandle).thenReturn(mock())
         whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
             .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
-        val scope = CoroutineScope(IMMEDIATE)
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
         val localUserSelectionManager =
             KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
@@ -143,7 +147,7 @@
             )
         val remoteUserSelectionManager =
             KeyguardQuickAffordanceRemoteUserSelectionManager(
-                scope = scope,
+                scope = testScope.backgroundScope,
                 userTracker = userTracker,
                 clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
                 userHandle = UserHandle.SYSTEM,
@@ -151,14 +155,14 @@
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
-                scope = scope,
+                scope = testScope.backgroundScope,
                 localUserSelectionManager = localUserSelectionManager,
                 remoteUserSelectionManager = remoteUserSelectionManager,
                 userTracker = userTracker,
                 legacySettingSyncer =
                     KeyguardQuickAffordanceLegacySettingSyncer(
-                        scope = scope,
-                        backgroundDispatcher = IMMEDIATE,
+                        scope = testScope.backgroundScope,
+                        backgroundDispatcher = testDispatcher,
                         secureSettings = FakeSettings(),
                         selectionsManager = localUserSelectionManager,
                     ),
@@ -194,366 +198,394 @@
     }
 
     @Test
-    fun `startButton - present - visible model - starts activity on click`() = runBlockingTest {
-        repository.setKeyguardShowing(true)
-        var latest: KeyguardQuickAffordanceViewModel? = null
-        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+    fun `startButton - present - visible model - starts activity on click`() =
+        testScope.runTest {
+            repository.setKeyguardShowing(true)
+            val latest = collectLastValue(underTest.startButton)
 
-        val testConfig =
-            TestConfig(
-                isVisible = true,
-                isClickable = true,
-                isActivated = true,
-                icon = mock(),
-                canShowWhileLocked = false,
-                intent = Intent("action"),
-            )
-        val configKey =
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                testConfig = testConfig,
-            )
-
-        assertQuickAffordanceViewModel(
-            viewModel = latest,
-            testConfig = testConfig,
-            configKey = configKey,
-        )
-        job.cancel()
-    }
-
-    @Test
-    fun `endButton - present - visible model - do nothing on click`() = runBlockingTest {
-        repository.setKeyguardShowing(true)
-        var latest: KeyguardQuickAffordanceViewModel? = null
-        val job = underTest.endButton.onEach { latest = it }.launchIn(this)
-
-        val config =
-            TestConfig(
-                isVisible = true,
-                isClickable = true,
-                icon = mock(),
-                canShowWhileLocked = false,
-                intent = null, // This will cause it to tell the system that the click was handled.
-            )
-        val configKey =
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_END,
-                testConfig = config,
-            )
-
-        assertQuickAffordanceViewModel(
-            viewModel = latest,
-            testConfig = config,
-            configKey = configKey,
-        )
-        job.cancel()
-    }
-
-    @Test
-    fun `startButton - not present - model is hidden`() = runBlockingTest {
-        var latest: KeyguardQuickAffordanceViewModel? = null
-        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
-
-        val config =
-            TestConfig(
-                isVisible = false,
-            )
-        val configKey =
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                testConfig = config,
-            )
-
-        assertQuickAffordanceViewModel(
-            viewModel = latest,
-            testConfig = config,
-            configKey = configKey,
-        )
-        job.cancel()
-    }
-
-    @Test
-    fun animateButtonReveal() = runBlockingTest {
-        repository.setKeyguardShowing(true)
-        val testConfig =
-            TestConfig(
-                isVisible = true,
-                isClickable = true,
-                icon = mock(),
-                canShowWhileLocked = false,
-                intent = Intent("action"),
-            )
-
-        setUpQuickAffordanceModel(
-            position = KeyguardQuickAffordancePosition.BOTTOM_START,
-            testConfig = testConfig,
-        )
-
-        val values = mutableListOf<Boolean>()
-        val job = underTest.startButton.onEach { values.add(it.animateReveal) }.launchIn(this)
-
-        repository.setAnimateDozingTransitions(true)
-        yield()
-        repository.setAnimateDozingTransitions(false)
-        yield()
-
-        // Note the extra false value in the beginning. This is to cover for the initial value
-        // inserted by the quick affordance interactor which it does to cover for config
-        // implementations that don't emit an initial value.
-        assertThat(values).isEqualTo(listOf(false, false, true, false))
-        job.cancel()
-    }
-
-    @Test
-    fun isOverlayContainerVisible() = runBlockingTest {
-        val values = mutableListOf<Boolean>()
-        val job = underTest.isOverlayContainerVisible.onEach(values::add).launchIn(this)
-
-        repository.setDozing(true)
-        repository.setDozing(false)
-
-        assertThat(values).isEqualTo(listOf(true, false, true))
-        job.cancel()
-    }
-
-    @Test
-    fun alpha() = runBlockingTest {
-        val values = mutableListOf<Float>()
-        val job = underTest.alpha.onEach(values::add).launchIn(this)
-
-        repository.setBottomAreaAlpha(0.1f)
-        repository.setBottomAreaAlpha(0.5f)
-        repository.setBottomAreaAlpha(0.2f)
-        repository.setBottomAreaAlpha(0f)
-
-        assertThat(values).isEqualTo(listOf(1f, 0.1f, 0.5f, 0.2f, 0f))
-        job.cancel()
-    }
-
-    @Test
-    fun isIndicationAreaPadded() = runBlockingTest {
-        repository.setKeyguardShowing(true)
-        val values = mutableListOf<Boolean>()
-        val job = underTest.isIndicationAreaPadded.onEach(values::add).launchIn(this)
-
-        setUpQuickAffordanceModel(
-            position = KeyguardQuickAffordancePosition.BOTTOM_START,
-            testConfig =
+            val testConfig =
                 TestConfig(
                     isVisible = true,
                     isClickable = true,
+                    isActivated = true,
                     icon = mock(),
-                    canShowWhileLocked = true,
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
                 )
-        )
-        setUpQuickAffordanceModel(
-            position = KeyguardQuickAffordancePosition.BOTTOM_END,
-            testConfig =
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = testConfig,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest(),
+                testConfig = testConfig,
+                configKey = configKey,
+            )
+        }
+
+    @Test
+    fun `startButton - in preview mode - visible even when keyguard not showing`() =
+        testScope.runTest {
+            underTest.enablePreviewMode(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START)
+            repository.setKeyguardShowing(false)
+            val latest = collectLastValue(underTest.startButton)
+
+            val icon: Icon = mock()
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig =
+                        TestConfig(
+                            isVisible = true,
+                            isClickable = true,
+                            isActivated = true,
+                            icon = icon,
+                            canShowWhileLocked = false,
+                            intent = Intent("action"),
+                        ),
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest(),
+                testConfig =
+                    TestConfig(
+                        isVisible = true,
+                        isClickable = false,
+                        isActivated = true,
+                        icon = icon,
+                        canShowWhileLocked = false,
+                        intent = Intent("action"),
+                    ),
+                configKey = configKey,
+            )
+            assertThat(latest()?.isSelected).isTrue()
+        }
+
+    @Test
+    fun `endButton - present - visible model - do nothing on click`() =
+        testScope.runTest {
+            repository.setKeyguardShowing(true)
+            val latest = collectLastValue(underTest.endButton)
+
+            val config =
                 TestConfig(
                     isVisible = true,
                     isClickable = true,
                     icon = mock(),
                     canShowWhileLocked = false,
+                    intent =
+                        null, // This will cause it to tell the system that the click was handled.
                 )
-        )
-        setUpQuickAffordanceModel(
-            position = KeyguardQuickAffordancePosition.BOTTOM_START,
-            testConfig =
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_END,
+                    testConfig = config,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest(),
+                testConfig = config,
+                configKey = configKey,
+            )
+        }
+
+    @Test
+    fun `startButton - not present - model is hidden`() =
+        testScope.runTest {
+            val latest = collectLastValue(underTest.startButton)
+
+            val config =
                 TestConfig(
                     isVisible = false,
                 )
-        )
-        setUpQuickAffordanceModel(
-            position = KeyguardQuickAffordancePosition.BOTTOM_END,
-            testConfig =
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = config,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest(),
+                testConfig = config,
+                configKey = configKey,
+            )
+        }
+
+    @Test
+    fun animateButtonReveal() =
+        testScope.runTest {
+            repository.setKeyguardShowing(true)
+            val testConfig =
                 TestConfig(
-                    isVisible = false,
+                    isVisible = true,
+                    isClickable = true,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
                 )
-        )
 
-        assertThat(values)
-            .isEqualTo(
-                listOf(
-                    // Initially, no button is visible so the indication area is not padded.
-                    false,
-                    // Once we add the first visible button, the indication area becomes padded.
-                    // This
-                    // continues to be true after we add the second visible button and even after we
-                    // make the first button not visible anymore.
-                    true,
-                    // Once both buttons are not visible, the indication area is, again, not padded.
-                    false,
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig = testConfig,
+            )
+
+            val value = collectLastValue(underTest.startButton.map { it.animateReveal })
+
+            assertThat(value()).isFalse()
+            repository.setAnimateDozingTransitions(true)
+            assertThat(value()).isTrue()
+            repository.setAnimateDozingTransitions(false)
+            assertThat(value()).isFalse()
+        }
+
+    @Test
+    fun isOverlayContainerVisible() =
+        testScope.runTest {
+            val value = collectLastValue(underTest.isOverlayContainerVisible)
+
+            assertThat(value()).isTrue()
+            repository.setDozing(true)
+            assertThat(value()).isFalse()
+            repository.setDozing(false)
+            assertThat(value()).isTrue()
+        }
+
+    @Test
+    fun alpha() =
+        testScope.runTest {
+            val value = collectLastValue(underTest.alpha)
+
+            assertThat(value()).isEqualTo(1f)
+            repository.setBottomAreaAlpha(0.1f)
+            assertThat(value()).isEqualTo(0.1f)
+            repository.setBottomAreaAlpha(0.5f)
+            assertThat(value()).isEqualTo(0.5f)
+            repository.setBottomAreaAlpha(0.2f)
+            assertThat(value()).isEqualTo(0.2f)
+            repository.setBottomAreaAlpha(0f)
+            assertThat(value()).isEqualTo(0f)
+        }
+
+    @Test
+    fun `alpha - in preview mode - does not change`() =
+        testScope.runTest {
+            underTest.enablePreviewMode(null)
+            val value = collectLastValue(underTest.alpha)
+
+            assertThat(value()).isEqualTo(1f)
+            repository.setBottomAreaAlpha(0.1f)
+            assertThat(value()).isEqualTo(1f)
+            repository.setBottomAreaAlpha(0.5f)
+            assertThat(value()).isEqualTo(1f)
+            repository.setBottomAreaAlpha(0.2f)
+            assertThat(value()).isEqualTo(1f)
+            repository.setBottomAreaAlpha(0f)
+            assertThat(value()).isEqualTo(1f)
+        }
+
+    @Test
+    fun isIndicationAreaPadded() =
+        testScope.runTest {
+            repository.setKeyguardShowing(true)
+            val value = collectLastValue(underTest.isIndicationAreaPadded)
+
+            assertThat(value()).isFalse()
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig =
+                    TestConfig(
+                        isVisible = true,
+                        isClickable = true,
+                        icon = mock(),
+                        canShowWhileLocked = true,
+                    )
+            )
+            assertThat(value()).isTrue()
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_END,
+                testConfig =
+                    TestConfig(
+                        isVisible = true,
+                        isClickable = true,
+                        icon = mock(),
+                        canShowWhileLocked = false,
+                    )
+            )
+            assertThat(value()).isTrue()
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig =
+                    TestConfig(
+                        isVisible = false,
+                    )
+            )
+            assertThat(value()).isTrue()
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_END,
+                testConfig =
+                    TestConfig(
+                        isVisible = false,
+                    )
+            )
+            assertThat(value()).isFalse()
+        }
+
+    @Test
+    fun indicationAreaTranslationX() =
+        testScope.runTest {
+            val value = collectLastValue(underTest.indicationAreaTranslationX)
+
+            assertThat(value()).isEqualTo(0f)
+            repository.setClockPosition(100, 100)
+            assertThat(value()).isEqualTo(100f)
+            repository.setClockPosition(200, 100)
+            assertThat(value()).isEqualTo(200f)
+            repository.setClockPosition(200, 200)
+            assertThat(value()).isEqualTo(200f)
+            repository.setClockPosition(300, 100)
+            assertThat(value()).isEqualTo(300f)
+        }
+
+    @Test
+    fun indicationAreaTranslationY() =
+        testScope.runTest {
+            val value =
+                collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET))
+
+            // Negative 0 - apparently there's a difference in floating point arithmetic - FML
+            assertThat(value()).isEqualTo(-0f)
+            val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f)
+            assertThat(value()).isEqualTo(expected1)
+            val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f)
+            assertThat(value()).isEqualTo(expected2)
+            val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f)
+            assertThat(value()).isEqualTo(expected3)
+            val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f)
+            assertThat(value()).isEqualTo(expected4)
+        }
+
+    @Test
+    fun `isClickable - true when alpha at threshold`() =
+        testScope.runTest {
+            repository.setKeyguardShowing(true)
+            repository.setBottomAreaAlpha(
+                KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
+            )
+
+            val testConfig =
+                TestConfig(
+                    isVisible = true,
+                    isClickable = true,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
                 )
-            )
-        job.cancel()
-    }
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = testConfig,
+                )
 
-    @Test
-    fun indicationAreaTranslationX() = runBlockingTest {
-        val values = mutableListOf<Float>()
-        val job = underTest.indicationAreaTranslationX.onEach(values::add).launchIn(this)
+            val latest = collectLastValue(underTest.startButton)
 
-        repository.setClockPosition(100, 100)
-        repository.setClockPosition(200, 100)
-        repository.setClockPosition(200, 200)
-        repository.setClockPosition(300, 100)
-
-        assertThat(values).isEqualTo(listOf(0f, 100f, 200f, 300f))
-        job.cancel()
-    }
-
-    @Test
-    fun indicationAreaTranslationY() = runBlockingTest {
-        val values = mutableListOf<Float>()
-        val job =
-            underTest
-                .indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET)
-                .onEach(values::add)
-                .launchIn(this)
-
-        val expectedTranslationValues =
-            listOf(
-                -0f, // Negative 0 - apparently there's a difference in floating point arithmetic -
-                // FML
-                setDozeAmountAndCalculateExpectedTranslationY(0.1f),
-                setDozeAmountAndCalculateExpectedTranslationY(0.2f),
-                setDozeAmountAndCalculateExpectedTranslationY(0.5f),
-                setDozeAmountAndCalculateExpectedTranslationY(1f),
-            )
-
-        assertThat(values).isEqualTo(expectedTranslationValues)
-        job.cancel()
-    }
-
-    @Test
-    fun `isClickable - true when alpha at threshold`() = runBlockingTest {
-        repository.setKeyguardShowing(true)
-        repository.setBottomAreaAlpha(
-            KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
-        )
-
-        val testConfig =
-            TestConfig(
-                isVisible = true,
-                isClickable = true,
-                icon = mock(),
-                canShowWhileLocked = false,
-                intent = Intent("action"),
-            )
-        val configKey =
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+            assertQuickAffordanceViewModel(
+                viewModel = latest(),
                 testConfig = testConfig,
+                configKey = configKey,
             )
-
-        var latest: KeyguardQuickAffordanceViewModel? = null
-        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
-        // The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't
-        // produce an initial value. We yield to give the coroutine time to emit the first real
-        // value from our config.
-        yield()
-
-        assertQuickAffordanceViewModel(
-            viewModel = latest,
-            testConfig = testConfig,
-            configKey = configKey,
-        )
-        job.cancel()
-    }
+        }
 
     @Test
-    fun `isClickable - true when alpha above threshold`() = runBlockingTest {
-        repository.setKeyguardShowing(true)
-        var latest: KeyguardQuickAffordanceViewModel? = null
-        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
-        repository.setBottomAreaAlpha(
-            min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f),
-        )
-
-        val testConfig =
-            TestConfig(
-                isVisible = true,
-                isClickable = true,
-                icon = mock(),
-                canShowWhileLocked = false,
-                intent = Intent("action"),
+    fun `isClickable - true when alpha above threshold`() =
+        testScope.runTest {
+            repository.setKeyguardShowing(true)
+            val latest = collectLastValue(underTest.startButton)
+            repository.setBottomAreaAlpha(
+                min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f),
             )
-        val configKey =
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+
+            val testConfig =
+                TestConfig(
+                    isVisible = true,
+                    isClickable = true,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
+                )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = testConfig,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest(),
                 testConfig = testConfig,
+                configKey = configKey,
             )
-
-        assertQuickAffordanceViewModel(
-            viewModel = latest,
-            testConfig = testConfig,
-            configKey = configKey,
-        )
-        job.cancel()
-    }
+        }
 
     @Test
-    fun `isClickable - false when alpha below threshold`() = runBlockingTest {
-        repository.setKeyguardShowing(true)
-        var latest: KeyguardQuickAffordanceViewModel? = null
-        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
-        repository.setBottomAreaAlpha(
-            max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f),
-        )
-
-        val testConfig =
-            TestConfig(
-                isVisible = true,
-                isClickable = false,
-                icon = mock(),
-                canShowWhileLocked = false,
-                intent = Intent("action"),
+    fun `isClickable - false when alpha below threshold`() =
+        testScope.runTest {
+            repository.setKeyguardShowing(true)
+            val latest = collectLastValue(underTest.startButton)
+            repository.setBottomAreaAlpha(
+                max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f),
             )
-        val configKey =
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+
+            val testConfig =
+                TestConfig(
+                    isVisible = true,
+                    isClickable = false,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
+                )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = testConfig,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest(),
                 testConfig = testConfig,
+                configKey = configKey,
             )
-
-        assertQuickAffordanceViewModel(
-            viewModel = latest,
-            testConfig = testConfig,
-            configKey = configKey,
-        )
-        job.cancel()
-    }
+        }
 
     @Test
-    fun `isClickable - false when alpha at zero`() = runBlockingTest {
-        repository.setKeyguardShowing(true)
-        var latest: KeyguardQuickAffordanceViewModel? = null
-        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
-        repository.setBottomAreaAlpha(0f)
+    fun `isClickable - false when alpha at zero`() =
+        testScope.runTest {
+            repository.setKeyguardShowing(true)
+            val latest = collectLastValue(underTest.startButton)
+            repository.setBottomAreaAlpha(0f)
 
-        val testConfig =
-            TestConfig(
-                isVisible = true,
-                isClickable = false,
-                icon = mock(),
-                canShowWhileLocked = false,
-                intent = Intent("action"),
-            )
-        val configKey =
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+            val testConfig =
+                TestConfig(
+                    isVisible = true,
+                    isClickable = false,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
+                )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = testConfig,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest(),
                 testConfig = testConfig,
+                configKey = configKey,
             )
+        }
 
-        assertQuickAffordanceViewModel(
-            viewModel = latest,
-            testConfig = testConfig,
-            configKey = configKey,
-        )
-        job.cancel()
-    }
-
-    private suspend fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
+    private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
         repository.setDozeAmount(dozeAmount)
         return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
     }
@@ -583,7 +615,6 @@
                         when (testConfig.isActivated) {
                             true -> ActivationState.Active
                             false -> ActivationState.Inactive
-                            null -> ActivationState.NotSupported
                         }
                 )
             } else {
@@ -636,6 +667,5 @@
     companion object {
         private const val DEFAULT_BURN_IN_OFFSET = 5
         private const val RETURNED_BURN_IN_OFFSET = 3
-        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
index e009e86..0e7bf8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogcatEchoTracker
+import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -33,7 +34,7 @@
 class MediaTttLoggerTest : SysuiTestCase() {
 
     private lateinit var buffer: LogBuffer
-    private lateinit var logger: MediaTttLogger
+    private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
 
     @Before
     fun setUp () {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index cce3e36..561867f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -40,7 +41,7 @@
     private lateinit var appIconFromPackageName: Drawable
     @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var applicationInfo: ApplicationInfo
-    @Mock private lateinit var logger: MediaTttLogger
+    @Mock private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
index 4aa982e..bad3f03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
@@ -27,13 +27,14 @@
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.android.systemui.util.wakelock.WakeLock
 
 class FakeMediaTttChipControllerReceiver(
     commandQueue: CommandQueue,
     context: Context,
-    logger: MediaTttLogger,
+    logger: MediaTttLogger<ChipReceiverInfo>,
     windowManager: WindowManager,
     mainExecutor: DelayableExecutor,
     accessibilityManager: AccessibilityManager,
@@ -44,6 +45,7 @@
     uiEventLogger: MediaTttReceiverUiEventLogger,
     viewUtil: ViewUtil,
     wakeLockBuilder: WakeLock.Builder,
+    systemClock: SystemClock,
 ) :
     MediaTttChipControllerReceiver(
         commandQueue,
@@ -59,6 +61,7 @@
         uiEventLogger,
         viewUtil,
         wakeLockBuilder,
+        systemClock,
     ) {
     override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index 23f7cdb..ef0bfb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -67,7 +67,7 @@
     @Mock
     private lateinit var applicationInfo: ApplicationInfo
     @Mock
-    private lateinit var logger: MediaTttLogger
+    private lateinit var logger: MediaTttLogger<ChipReceiverInfo>
     @Mock
     private lateinit var accessibilityManager: AccessibilityManager
     @Mock
@@ -128,6 +128,7 @@
             receiverUiEventLogger,
             viewUtil,
             fakeWakeLockBuilder,
+            fakeClock,
         )
         controllerReceiver.start()
 
@@ -155,6 +156,7 @@
             receiverUiEventLogger,
             viewUtil,
             fakeWakeLockBuilder,
+            fakeClock,
         )
         controllerReceiver.start()
 
@@ -193,6 +195,36 @@
     }
 
     @Test
+    fun commandQueueCallback_transferToReceiverSucceeded_noChipShown() {
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+                StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+                routeInfo,
+                null,
+                null
+        )
+
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
+                MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_SUCCEEDED.id
+        )
+    }
+
+    @Test
+    fun commandQueueCallback_transferToReceiverFailed_noChipShown() {
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+                StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED,
+                routeInfo,
+                null,
+                null
+        )
+
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
+                MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_FAILED.id
+        )
+    }
+
+    @Test
     fun commandQueueCallback_closeThenFar_chipShownThenHidden() {
         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
@@ -214,6 +246,48 @@
     }
 
     @Test
+    fun commandQueueCallback_closeThenSucceeded_chipShownThenHidden() {
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+            StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
+            routeInfo,
+            null,
+            null
+        )
+
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+            StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            null,
+            null
+        )
+
+        val viewCaptor = ArgumentCaptor.forClass(View::class.java)
+        verify(windowManager).addView(viewCaptor.capture(), any())
+        verify(windowManager).removeView(viewCaptor.value)
+    }
+
+    @Test
+    fun commandQueueCallback_closeThenFailed_chipShownThenHidden() {
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+            StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
+            routeInfo,
+            null,
+            null
+        )
+
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+            StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED,
+            routeInfo,
+            null,
+            null
+        )
+
+        val viewCaptor = ArgumentCaptor.forClass(View::class.java)
+        verify(windowManager).addView(viewCaptor.capture(), any())
+        verify(windowManager).removeView(viewCaptor.value)
+    }
+
+    @Test
     fun commandQueueCallback_closeThenFar_wakeLockAcquiredThenReleased() {
         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
                 StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
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 311740e..b03a545 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
@@ -45,6 +45,7 @@
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
 import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
 import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -83,7 +84,7 @@
     @Mock private lateinit var falsingManager: FalsingManager
     @Mock private lateinit var falsingCollector: FalsingCollector
     @Mock private lateinit var chipbarLogger: ChipbarLogger
-    @Mock private lateinit var logger: MediaTttLogger
+    @Mock private lateinit var logger: MediaTttLogger<ChipbarInfo>
     @Mock private lateinit var mediaTttFlags: MediaTttFlags
     @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var powerManager: PowerManager
@@ -142,6 +143,7 @@
                 viewUtil,
                 vibratorHelper,
                 fakeWakeLockBuilder,
+                fakeClock,
             )
         chipbarCoordinator.start()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index 9f28708..5e082f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -1,9 +1,12 @@
 package com.android.systemui.qs
 
+import android.content.res.Configuration
 import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
+import android.testing.TestableResources
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
@@ -26,10 +29,11 @@
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.any
+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.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -54,8 +58,11 @@
     @Mock private lateinit var otherTile: QSTile
     @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
     @Mock private lateinit var featureFlags: FeatureFlags
+    @Mock private lateinit var configuration: Configuration
+    @Mock private lateinit var pagedTileLayout: PagedTileLayout
 
     private lateinit var controller: QSPanelController
+    private val testableResources: TestableResources = mContext.orCreateTestableResources
 
     @Before
     fun setUp() {
@@ -63,7 +70,9 @@
 
         whenever(brightnessSliderFactory.create(any(), any())).thenReturn(brightnessSlider)
         whenever(brightnessControllerFactory.create(any())).thenReturn(brightnessController)
-        whenever(qsPanel.resources).thenReturn(mContext.orCreateTestableResources.resources)
+        testableResources.addOverride(R.bool.config_use_split_notification_shade, false)
+        whenever(qsPanel.resources).thenReturn(testableResources.resources)
+        whenever(qsPanel.getOrCreateTileLayout()).thenReturn(pagedTileLayout)
         whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false)
         whenever(qsPanel.setListening(anyBoolean())).then {
             whenever(qsPanel.isListening).thenReturn(it.getArgument(0))
@@ -121,4 +130,15 @@
         whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false)
         assertThat(controller.isBouncerInTransit()).isEqualTo(false)
     }
+
+    @Test
+    fun configurationChange_onlySplitShadeConfigChanges_tileAreRedistributed() {
+        testableResources.addOverride(R.bool.config_use_split_notification_shade, false)
+        controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+        verify(pagedTileLayout, never()).forceTilesRedistribution()
+
+        testableResources.addOverride(R.bool.config_use_split_notification_shade, true)
+        controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+        verify(pagedTileLayout).forceTilesRedistribution()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
index 3ae8428..b59005a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
@@ -86,7 +86,8 @@
     private View mDialogView;
     private View mSubTitle;
     private LinearLayout mEthernet;
-    private LinearLayout mMobileDataToggle;
+    private LinearLayout mMobileDataLayout;
+    private Switch mMobileToggleSwitch;
     private LinearLayout mWifiToggle;
     private Switch mWifiToggleSwitch;
     private TextView mWifiToggleSummary;
@@ -135,7 +136,8 @@
         mDialogView = mInternetDialog.mDialogView;
         mSubTitle = mDialogView.requireViewById(R.id.internet_dialog_subtitle);
         mEthernet = mDialogView.requireViewById(R.id.ethernet_layout);
-        mMobileDataToggle = mDialogView.requireViewById(R.id.mobile_network_layout);
+        mMobileDataLayout = mDialogView.requireViewById(R.id.mobile_network_layout);
+        mMobileToggleSwitch = mDialogView.requireViewById(R.id.mobile_toggle);
         mWifiToggle = mDialogView.requireViewById(R.id.turn_on_wifi_layout);
         mWifiToggleSwitch = mDialogView.requireViewById(R.id.wifi_toggle);
         mWifiToggleSummary = mDialogView.requireViewById(R.id.wifi_toggle_summary);
@@ -236,7 +238,7 @@
 
         mInternetDialog.updateDialog(true);
 
-        assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.GONE);
     }
 
     @Test
@@ -248,7 +250,7 @@
 
         mInternetDialog.updateDialog(true);
 
-        assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.GONE);
 
         // Carrier network should be visible if airplane mode ON and Wi-Fi is ON.
         when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true);
@@ -257,7 +259,7 @@
 
         mInternetDialog.updateDialog(true);
 
-        assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
     @Test
@@ -267,7 +269,7 @@
 
         mInternetDialog.updateDialog(true);
 
-        assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.GONE);
     }
 
     @Test
@@ -279,7 +281,7 @@
 
         mInternetDialog.updateDialog(true);
 
-        assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.VISIBLE);
         assertThat(mAirplaneModeSummaryText.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
@@ -316,6 +318,30 @@
     }
 
     @Test
+    public void updateDialog_mobileDataIsEnabled_checkMobileDataSwitch() {
+        doReturn(true).when(mInternetDialogController).hasActiveSubId();
+        when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true);
+        when(mInternetDialogController.isMobileDataEnabled()).thenReturn(true);
+        mMobileToggleSwitch.setChecked(false);
+
+        mInternetDialog.updateDialog(true);
+
+        assertThat(mMobileToggleSwitch.isChecked()).isTrue();
+    }
+
+    @Test
+    public void updateDialog_mobileDataIsNotChanged_checkMobileDataSwitch() {
+        doReturn(true).when(mInternetDialogController).hasActiveSubId();
+        when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true);
+        when(mInternetDialogController.isMobileDataEnabled()).thenReturn(false);
+        mMobileToggleSwitch.setChecked(false);
+
+        mInternetDialog.updateDialog(true);
+
+        assertThat(mMobileToggleSwitch.isChecked()).isFalse();
+    }
+
+    @Test
     public void updateDialog_wifiOnAndHasInternetWifi_showConnectedWifi() {
         mInternetDialog.dismissDialog();
         doReturn(true).when(mInternetDialogController).hasActiveSubId();
@@ -695,7 +721,7 @@
     private void setNetworkVisible(boolean ethernetVisible, boolean mobileDataVisible,
             boolean connectedWifiVisible) {
         mEthernet.setVisibility(ethernetVisible ? View.VISIBLE : View.GONE);
-        mMobileDataToggle.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE);
+        mMobileDataLayout.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE);
         mConnectedWifi.setVisibility(connectedWifiVisible ? View.VISIBLE : View.GONE);
     }
 }
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 d928f6f..6a7308c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -1105,6 +1105,17 @@
 
         mStatusBarStateController.setState(KEYGUARD);
 
+        assertThat(mNotificationPanelViewController.isQsExpanded()).isEqualTo(false);
+        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isEqualTo(false);
+    }
+
+    @Test
+    public void testLockedSplitShadeTransitioningToKeyguard_closesQS() {
+        enableSplitShade(true);
+        mStatusBarStateController.setState(SHADE_LOCKED);
+        mNotificationPanelViewController.setQsExpanded(true);
+
+        mStatusBarStateController.setState(KEYGUARD);
 
         assertThat(mNotificationPanelViewController.isQsExpanded()).isEqualTo(false);
         assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isEqualTo(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index aa1114b..cb4f119 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -38,6 +39,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NodeController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
 import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback
 import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
 import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -88,6 +90,7 @@
     private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock()
     private val mHeaderController: NodeController = mock()
     private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider = mock()
+    private val mFlags: NotifPipelineFlags = mock()
 
     private lateinit var mEntry: NotificationEntry
     private lateinit var mGroupSummary: NotificationEntry
@@ -113,6 +116,7 @@
             mNotificationInterruptStateProvider,
             mRemoteInputManager,
             mLaunchFullScreenIntentProvider,
+            mFlags,
             mHeaderController,
             mExecutor)
         mCoordinator.attach(mNotifPipeline)
@@ -246,14 +250,14 @@
 
     @Test
     fun testOnEntryAdded_shouldFullScreen() {
-        setShouldFullScreen(mEntry)
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_EXPECTED_NOT_TO_HUN)
         mCollectionListener.onEntryAdded(mEntry)
         verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
     }
 
     @Test
     fun testOnEntryAdded_shouldNotFullScreen() {
-        setShouldFullScreen(mEntry, should = false)
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
         mCollectionListener.onEntryAdded(mEntry)
         verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
     }
@@ -805,15 +809,96 @@
         verify(mHeadsUpManager, never()).showNotification(any())
     }
 
+    @Test
+    fun testOnRankingApplied_noFSIOnUpdateWhenFlagOff() {
+        // Ensure the feature flag is off
+        whenever(mFlags.fsiOnDNDUpdate()).thenReturn(false)
+
+        // GIVEN that mEntry was previously suppressed from full-screen only by DND
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // and it is then updated to allow full screen
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+        whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+        mCollectionListener.onRankingApplied()
+
+        // THEN it should not full screen because the feature is off
+        verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+    }
+
+    @Test
+    fun testOnRankingApplied_updateToFullScreen() {
+        // Turn on the feature
+        whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+        // GIVEN that mEntry was previously suppressed from full-screen only by DND
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // at this point, it should not have full screened
+        verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+
+        // and it is then updated to allow full screen AND HUN
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+        setShouldHeadsUp(mEntry)
+        whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+        mCollectionListener.onRankingApplied()
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN it should full screen but it should NOT HUN
+        verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+        verify(mHeadsUpManager, never()).showNotification(any())
+    }
+
+    @Test
+    fun testOnRankingApplied_noFSIWhenAlsoSuppressedForOtherReasons() {
+        // Feature on
+        whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+        // GIVEN that mEntry is suppressed by DND (functionally), but not *only* DND
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_BY_DND)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // and it is updated to full screen later
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+        mCollectionListener.onRankingApplied()
+
+        // THEN it should still not full screen because something else was blocking it before
+        verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+    }
+
+    @Test
+    fun testOnRankingApplied_noFSIWhenTooOld() {
+        // Feature on
+        whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+        // GIVEN that mEntry is suppressed only by DND
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // but it's >10s old
+        mCoordinator.addForFSIReconsideration(mEntry, mSystemClock.currentTimeMillis() - 10000)
+
+        // and it is updated to full screen later
+        setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_EXPECTED_NOT_TO_HUN)
+        mCollectionListener.onRankingApplied()
+
+        // THEN it should still not full screen because it's too old
+        verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+    }
+
     private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
         whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
         whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
                 .thenReturn(should)
     }
 
-    private fun setShouldFullScreen(entry: NotificationEntry, should: Boolean = true) {
-        whenever(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
-            .thenReturn(should)
+    private fun setShouldFullScreen(entry: NotificationEntry, decision: FullScreenIntentDecision) {
+        whenever(mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry))
+            .thenReturn(decision)
     }
 
     private fun finishBind(entry: NotificationEntry) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
new file mode 100644
index 0000000..33b94e3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.notification.logging
+
+import android.app.Notification
+import android.app.StatsManager
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.util.StatsEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationMemoryLoggerTest : SysuiTestCase() {
+
+    private val bgExecutor = FakeExecutor(FakeSystemClock())
+    private val immediate = Dispatchers.Main.immediate
+
+    @Mock private lateinit var statsManager: StatsManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun onInit_registersCallback() {
+        val logger = createLoggerWithNotifications(listOf())
+        logger.init()
+        verify(statsManager)
+            .setPullAtomCallback(SysUiStatsLog.NOTIFICATION_MEMORY_USE, null, bgExecutor, logger)
+    }
+
+    @Test
+    fun onPullAtom_wrongAtomId_returnsSkip() {
+        val logger = createLoggerWithNotifications(listOf())
+        val data: MutableList<StatsEvent> = mutableListOf()
+        assertThat(logger.onPullAtom(111, data)).isEqualTo(StatsManager.PULL_SKIP)
+        assertThat(data).isEmpty()
+    }
+
+    @Test
+    fun onPullAtom_emptyNotifications_returnsZeros() {
+        val logger = createLoggerWithNotifications(listOf())
+        val data: MutableList<StatsEvent> = mutableListOf()
+        assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data))
+            .isEqualTo(StatsManager.PULL_SUCCESS)
+        assertThat(data).isEmpty()
+    }
+
+    @Test
+    fun onPullAtom_notificationPassed_populatesData() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888))
+        val notification =
+            Notification.Builder(context).setSmallIcon(icon).setContentTitle("title").build()
+        val logger = createLoggerWithNotifications(listOf(notification))
+        val data: MutableList<StatsEvent> = mutableListOf()
+
+        assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data))
+            .isEqualTo(StatsManager.PULL_SUCCESS)
+        assertThat(data).hasSize(1)
+    }
+
+    @Test
+    fun onPullAtom_multipleNotificationsPassed_populatesData() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888))
+        val notification =
+            Notification.Builder(context).setSmallIcon(icon).setContentTitle("title").build()
+        val iconTwo = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888))
+
+        val notificationTwo =
+            Notification.Builder(context)
+                .setStyle(Notification.BigTextStyle().bigText("text"))
+                .setSmallIcon(iconTwo)
+                .setContentTitle("titleTwo")
+                .build()
+        val logger = createLoggerWithNotifications(listOf(notification, notificationTwo))
+        val data: MutableList<StatsEvent> = mutableListOf()
+
+        assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data))
+            .isEqualTo(StatsManager.PULL_SUCCESS)
+        assertThat(data).hasSize(2)
+    }
+
+    private fun createLoggerWithNotifications(
+        notifications: List<Notification>
+    ): NotificationMemoryLogger {
+        val pipeline: NotifPipeline = mock()
+        val notifications =
+            notifications.map { notification ->
+                NotificationEntryBuilder().setTag("test").setNotification(notification).build()
+            }
+        whenever(pipeline.allNotifs).thenReturn(notifications)
+        return NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
index f69839b..072a497 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
@@ -23,6 +23,7 @@
 import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.drawable.Icon
+import android.stats.sysui.NotificationEnums
 import android.testing.AndroidTestingRunner
 import android.widget.RemoteViews
 import androidx.test.filters.SmallTest
@@ -50,7 +51,27 @@
             extras = 3316,
             bigPicture = 0,
             extender = 0,
-            style = null,
+            style = NotificationEnums.STYLE_NONE,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_rankerGroupNotification() {
+        val notification = createBasicNotification().build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(
+                createNotificationEntry(createBasicNotification().setGroup("ranker_group").build())
+            )
+        assertNotificationObjectSizes(
+            memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = NotificationEnums.STYLE_RANKER_GROUP,
             styleIcon = 0,
             hasCustomView = false,
         )
@@ -69,7 +90,7 @@
             extras = 3316,
             bigPicture = 0,
             extender = 0,
-            style = null,
+            style = NotificationEnums.STYLE_NONE,
             styleIcon = 0,
             hasCustomView = false,
         )
@@ -92,7 +113,7 @@
             extras = 3384,
             bigPicture = 0,
             extender = 0,
-            style = null,
+            style = NotificationEnums.STYLE_NONE,
             styleIcon = 0,
             hasCustomView = true,
         )
@@ -112,7 +133,7 @@
             extras = 3212,
             bigPicture = 0,
             extender = 0,
-            style = null,
+            style = NotificationEnums.STYLE_NONE,
             styleIcon = 0,
             hasCustomView = false,
         )
@@ -141,7 +162,7 @@
             extras = 4092,
             bigPicture = bigPicture.bitmap.allocationByteCount,
             extender = 0,
-            style = "BigPictureStyle",
+            style = NotificationEnums.STYLE_BIG_PICTURE,
             styleIcon = bigPictureIcon.bitmap.allocationByteCount,
             hasCustomView = false,
         )
@@ -167,7 +188,7 @@
             extras = 4084,
             bigPicture = 0,
             extender = 0,
-            style = "CallStyle",
+            style = NotificationEnums.STYLE_CALL,
             styleIcon = personIcon.bitmap.allocationByteCount,
             hasCustomView = false,
         )
@@ -203,7 +224,7 @@
             extras = 5024,
             bigPicture = 0,
             extender = 0,
-            style = "MessagingStyle",
+            style = NotificationEnums.STYLE_MESSAGING,
             styleIcon =
                 personIcon.bitmap.allocationByteCount +
                     historicPersonIcon.bitmap.allocationByteCount,
@@ -225,7 +246,7 @@
             extras = 3612,
             bigPicture = 0,
             extender = 556656,
-            style = null,
+            style = NotificationEnums.STYLE_NONE,
             styleIcon = 0,
             hasCustomView = false,
         )
@@ -246,7 +267,7 @@
             extras = 3820,
             bigPicture = 0,
             extender = 388 + wearBackground.allocationByteCount,
-            style = null,
+            style = NotificationEnums.STYLE_NONE,
             styleIcon = 0,
             hasCustomView = false,
         )
@@ -272,7 +293,7 @@
         extras: Int,
         bigPicture: Int,
         extender: Int,
-        style: String?,
+        style: Int,
         styleIcon: Int,
         hasCustomView: Boolean,
     ) {
@@ -282,11 +303,7 @@
         assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
         assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
         assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture)
-        if (style == null) {
-            assertThat(memoryUse.objectUsage.style).isNull()
-        } else {
-            assertThat(memoryUse.objectUsage.style).isEqualTo(style)
-        }
+        assertThat(memoryUse.objectUsage.style).isEqualTo(style)
         assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon)
         assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt
index 3a16fb3..a0f5048 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt
@@ -8,6 +8,7 @@
 import android.widget.RemoteViews
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.tests.R
 import com.google.common.truth.Truth.assertThat
@@ -39,16 +40,84 @@
     fun testViewWalker_plainNotification() {
         val row = testHelper.createRow()
         val result = NotificationMemoryViewWalker.getViewUsage(row)
-        assertThat(result).hasSize(5)
-        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
-        assertThat(result)
-            .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result).hasSize(3)
         assertThat(result)
             .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0))
         assertThat(result)
             .contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result).contains(NotificationViewUsage(ViewType.TOTAL, 0, 0, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun testViewWalker_plainNotification_withPublicView() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888))
+        val publicIcon = Icon.createWithBitmap(Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888))
+        testHelper.setDefaultInflationFlags(NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL)
+        val row =
+            testHelper.createRow(
+                Notification.Builder(mContext)
+                    .setContentText("Test")
+                    .setContentTitle("title")
+                    .setSmallIcon(icon)
+                    .setPublicVersion(
+                        Notification.Builder(mContext)
+                            .setContentText("Public Test")
+                            .setContentTitle("title")
+                            .setSmallIcon(publicIcon)
+                            .build()
+                    )
+                    .build()
+            )
+        val result = NotificationMemoryViewWalker.getViewUsage(row)
+        assertThat(result).hasSize(4)
         assertThat(result)
-            .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0))
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_EXPANDED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    0,
+                    icon.bitmap.allocationByteCount
+                )
+            )
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_CONTRACTED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    0,
+                    icon.bitmap.allocationByteCount
+                )
+            )
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PUBLIC_VIEW,
+                    publicIcon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    0,
+                    publicIcon.bitmap.allocationByteCount
+                )
+            )
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.TOTAL,
+                    icon.bitmap.allocationByteCount + publicIcon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    0,
+                    icon.bitmap.allocationByteCount + publicIcon.bitmap.allocationByteCount
+                )
+            )
     }
 
     @Test
@@ -67,7 +136,7 @@
                     .build()
             )
         val result = NotificationMemoryViewWalker.getViewUsage(row)
-        assertThat(result).hasSize(5)
+        assertThat(result).hasSize(3)
         assertThat(result)
             .contains(
                 NotificationViewUsage(
@@ -95,8 +164,20 @@
                     icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount
                 )
             )
-        // Due to deduplication, this should all be 0.
-        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.TOTAL,
+                    icon.bitmap.allocationByteCount,
+                    largeIcon.bitmap.allocationByteCount,
+                    0,
+                    bigPicture.allocationByteCount,
+                    0,
+                    bigPicture.allocationByteCount +
+                        icon.bitmap.allocationByteCount +
+                        largeIcon.bitmap.allocationByteCount
+                )
+            )
     }
 
     @Test
@@ -117,7 +198,7 @@
                     .build()
             )
         val result = NotificationMemoryViewWalker.getViewUsage(row)
-        assertThat(result).hasSize(5)
+        assertThat(result).hasSize(3)
         assertThat(result)
             .contains(
                 NotificationViewUsage(
@@ -142,7 +223,17 @@
                     bitmap.allocationByteCount + icon.bitmap.allocationByteCount
                 )
             )
-        // Due to deduplication, this should all be 0.
-        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.TOTAL,
+                    icon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    bitmap.allocationByteCount,
+                    bitmap.allocationByteCount + icon.bitmap.allocationByteCount
+                )
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 4d9db8c..5832569 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -518,7 +518,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = true,
                 fullyVisible = false,
-                headerVisibleAmount = 1f
+                headerVisibleAmount = 1f,
         )
         val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
         algorithmState.visibleChildren.add(childHunView)
@@ -526,7 +526,6 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -546,7 +545,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = true,
                 fullyVisible = false,
-                headerVisibleAmount = 1f
+                headerVisibleAmount = 1f,
         )
         // Use half of the HUN's height as overlap
         childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat()
@@ -556,7 +555,6 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -580,7 +578,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = true,
                 fullyVisible = true,
-                headerVisibleAmount = 1f
+                headerVisibleAmount = 1f,
         )
         // HUN doesn't overlap with QQS Panel
         childHunView.viewState.yTranslation = ambientState.topPadding +
@@ -591,7 +589,6 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -611,7 +608,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = false,
                 fullyVisible = false,
-                headerVisibleAmount = 0f
+                headerVisibleAmount = 0f,
         )
         childHunView.viewState.yTranslation = 0f
         // Shade is closed, thus childHunView's headerVisibleAmount is 0
@@ -622,7 +619,6 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -642,7 +638,7 @@
         val childHunView = createHunViewMock(
                 isShadeOpen = false,
                 fullyVisible = false,
-                headerVisibleAmount = 0.5f
+                headerVisibleAmount = 0.5f,
         )
         childHunView.viewState.yTranslation = 0f
         // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1
@@ -654,7 +650,6 @@
         // When: updateChildZValue() is called for the top HUN
         stackScrollAlgorithm.updateChildZValue(
                 /* i= */ 0,
-                /* childrenOnTop= */ 0.0f,
                 /* StackScrollAlgorithmState= */ algorithmState,
                 /* ambientState= */ ambientState,
                 /* shouldElevateHun= */ true
@@ -669,7 +664,7 @@
     private fun createHunViewMock(
             isShadeOpen: Boolean,
             fullyVisible: Boolean,
-            headerVisibleAmount: Float
+            headerVisibleAmount: Float,
     ) =
             mock<ExpandableNotificationRow>().apply {
                 val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
@@ -680,7 +675,10 @@
             }
 
 
-    private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) =
+    private fun createHunChildViewState(
+            isShadeOpen: Boolean,
+            fullyVisible: Boolean,
+    ) =
             ExpandableViewState().apply {
                 // Mock the HUN's height with ambientState.topPadding +
                 // ambientState.stackTranslation
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 3d9fd96..22c0ea1 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
@@ -16,11 +16,14 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.ui.view
 
+import android.content.res.ColorStateList
+import android.graphics.Rect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import android.testing.ViewUtils
 import android.view.View
+import android.widget.ImageView
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -44,6 +47,7 @@
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -229,10 +233,43 @@
         ViewUtils.detachView(view)
     }
 
+    @Test
+    fun onDarkChanged_iconHasNewColor() {
+        whenever(statusBarPipelineFlags.useWifiDebugColoring()).thenReturn(false)
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        val areas = ArrayList(listOf(Rect(0, 0, 1000, 1000)))
+        val color = 0x12345678
+        view.onDarkChanged(areas, 1.0f, color)
+        testableLooper.processAllMessages()
+
+        assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
+    }
+
+    @Test
+    fun setStaticDrawableColor_iconHasNewColor() {
+        whenever(statusBarPipelineFlags.useWifiDebugColoring()).thenReturn(false)
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        val color = 0x23456789
+        view.setStaticDrawableColor(color)
+        testableLooper.processAllMessages()
+
+        assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
+    }
+
     private fun View.getIconGroupView(): View {
         return this.requireViewById(R.id.wifi_group)
     }
 
+    private fun View.getIconView(): ImageView {
+        return this.requireViewById(R.id.wifi_signal)
+    }
+
     private fun View.getDotView(): View {
         return this.requireViewById(R.id.status_bar_dot)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 09f0d4a..82153d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.wakelock.WakeLock
 import com.android.systemui.util.wakelock.WakeLockFake
 import com.google.common.truth.Truth.assertThat
@@ -59,7 +60,7 @@
     private lateinit var fakeWakeLock: WakeLockFake
 
     @Mock
-    private lateinit var logger: TemporaryViewLogger
+    private lateinit var logger: TemporaryViewLogger<ViewInfo>
     @Mock
     private lateinit var accessibilityManager: AccessibilityManager
     @Mock
@@ -74,7 +75,7 @@
         MockitoAnnotations.initMocks(this)
 
         whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any()))
-            .thenReturn(TIMEOUT_MS.toInt())
+            .thenAnswer { it.arguments[0] }
 
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
@@ -84,14 +85,15 @@
         fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
 
         underTest = TestController(
-                context,
-                logger,
-                windowManager,
-                fakeExecutor,
-                accessibilityManager,
-                configurationController,
-                powerManager,
-                fakeWakeLockBuilder,
+            context,
+            logger,
+            windowManager,
+            fakeExecutor,
+            accessibilityManager,
+            configurationController,
+            powerManager,
+            fakeWakeLockBuilder,
+            fakeClock,
         )
         underTest.start()
     }
@@ -112,14 +114,14 @@
 
     @Test
     fun displayView_logged() {
-        underTest.displayView(
-            ViewInfo(
-                name = "name",
-                windowTitle = "Fake Window Title",
-            )
+        val info = ViewInfo(
+            name = "name",
+            windowTitle = "Fake Window Title",
         )
 
-        verify(logger).logViewAddition("id", "Fake Window Title")
+        underTest.displayView(info)
+
+        verify(logger).logViewAddition(info)
     }
 
     @Test
@@ -168,10 +170,11 @@
     }
 
     @Test
-    fun displayView_twiceWithDifferentWindowTitles_oldViewRemovedNewViewAdded() {
+    fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() {
         underTest.displayView(
             ViewInfo(
                 name = "name",
+                id = "First",
                 windowTitle = "First Fake Window Title",
             )
         )
@@ -179,6 +182,7 @@
         underTest.displayView(
             ViewInfo(
                 name = "name",
+                id = "Second",
                 windowTitle = "Second Fake Window Title",
             )
         )
@@ -263,19 +267,69 @@
     }
 
     @Test
+    fun viewUpdatedWithNewOnViewTimeoutRunnable_newRunnableUsed() {
+        var runnable1Run = false
+        underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
+            runnable1Run = true
+        }
+
+        var runnable2Run = false
+        underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
+            runnable2Run = true
+        }
+
+        fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+        assertThat(runnable1Run).isFalse()
+        assertThat(runnable2Run).isTrue()
+    }
+
+    @Test
+    fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() {
+        underTest.displayView(
+            ViewInfo(
+                name = "name",
+                windowTitle = "First Fake Window Title",
+                id = "id1"
+            )
+        )
+
+        underTest.displayView(
+            ViewInfo(
+                name = "name",
+                windowTitle = "Second Fake Window Title",
+                id = "id2"
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+
+        verify(windowManager, times(2)).addView(capture(viewCaptor), capture(windowParamsCaptor))
+
+        assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title")
+        assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
+        verify(windowManager).removeView(viewCaptor.allValues[0])
+        verify(configurationController, never()).removeCallback(any())
+    }
+
+    @Test
     fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
         underTest.displayView(ViewInfo("First name", id = "id1"))
 
         verify(windowManager).addView(any(), any())
-
         reset(windowManager)
+
         underTest.displayView(ViewInfo("Second name", id = "id2"))
+
+        verify(windowManager).removeView(any())
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
         underTest.removeView("id2", "test reason")
 
         verify(windowManager).removeView(any())
-
-        fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
-
+        verify(windowManager).addView(any(), any())
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
 
@@ -284,6 +338,7 @@
 
         verify(windowManager).removeView(any())
         assertThat(underTest.activeViews.size).isEqualTo(0)
+        verify(configurationController).removeCallback(any())
     }
 
     @Test
@@ -291,19 +346,28 @@
         underTest.displayView(ViewInfo("First name", id = "id1"))
 
         verify(windowManager).addView(any(), any())
-
         reset(windowManager)
+
         underTest.displayView(ViewInfo("Second name", id = "id2"))
+
+        verify(windowManager).removeView(any())
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
+        // WHEN an old view is removed
         underTest.removeView("id1", "test reason")
 
+        // THEN we don't update anything
         verify(windowManager, never()).removeView(any())
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
+        verify(configurationController, never()).removeCallback(any())
 
         fakeClock.advanceTime(TIMEOUT_MS + 1)
 
         verify(windowManager).removeView(any())
         assertThat(underTest.activeViews.size).isEqualTo(0)
+        verify(configurationController).removeCallback(any())
     }
 
     @Test
@@ -312,33 +376,31 @@
         underTest.displayView(ViewInfo("Second name", id = "id2"))
         underTest.displayView(ViewInfo("Third name", id = "id3"))
 
-        verify(windowManager).addView(any(), any())
+        verify(windowManager, times(3)).addView(any(), any())
+        verify(windowManager, times(2)).removeView(any())
 
         reset(windowManager)
         underTest.removeView("id3", "test reason")
 
         verify(windowManager).removeView(any())
-
-        fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
-
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
+        verify(configurationController, never()).removeCallback(any())
 
         reset(windowManager)
         underTest.removeView("id2", "test reason")
 
         verify(windowManager).removeView(any())
-
-        fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
-
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
+        verify(configurationController, never()).removeCallback(any())
 
         reset(windowManager)
         fakeClock.advanceTime(TIMEOUT_MS + 1)
 
         verify(windowManager).removeView(any())
         assertThat(underTest.activeViews.size).isEqualTo(0)
+        verify(configurationController).removeCallback(any())
     }
 
     @Test
@@ -347,18 +409,21 @@
         underTest.displayView(ViewInfo("New name", id = "id1"))
 
         verify(windowManager).addView(any(), any())
-
         reset(windowManager)
+
         underTest.displayView(ViewInfo("Second name", id = "id2"))
+
+        verify(windowManager).removeView(any())
+        verify(windowManager).addView(any(), any())
+        reset(windowManager)
+
         underTest.removeView("id2", "test reason")
 
         verify(windowManager).removeView(any())
-
-        fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
-
+        verify(windowManager).addView(any(), any())
         assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
         assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("New name")
-        assertThat(underTest.activeViews[0].second.name).isEqualTo("New name")
+        assertThat(underTest.activeViews[0].info.name).isEqualTo("New name")
 
         reset(windowManager)
         fakeClock.advanceTime(TIMEOUT_MS + 1)
@@ -368,19 +433,523 @@
     }
 
     @Test
-    fun multipleViewsWithDifferentIds_viewsTimeouts_noViewLeftToDisplay() {
-        underTest.displayView(ViewInfo("First name", id = "id1"))
-        fakeClock.advanceTime(TIMEOUT_MS / 3)
-        underTest.displayView(ViewInfo("Second name", id = "id2"))
-        fakeClock.advanceTime(TIMEOUT_MS / 3)
-        underTest.displayView(ViewInfo("Third name", id = "id3"))
+    fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() {
+        underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
+        fakeClock.advanceTime(1000)
+        underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000))
+        fakeClock.advanceTime(1000)
+        underTest.displayView(ViewInfo("Third name", id = "id3", timeoutMs = 20000))
 
         reset(windowManager)
-        fakeClock.advanceTime(TIMEOUT_MS + 1)
+        fakeClock.advanceTime(20000 + 1)
 
         verify(windowManager).removeView(any())
         verify(windowManager, never()).addView(any(), any())
         assertThat(underTest.activeViews.size).isEqualTo(0)
+        verify(configurationController).removeCallback(any())
+    }
+
+    @Test
+    fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() {
+        underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
+        fakeClock.advanceTime(1000)
+        underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500))
+
+        reset(windowManager)
+        fakeClock.advanceTime(2500 + 1)
+        // At this point, 3501ms have passed, so id1 only has 499ms left which is not enough.
+        // So, it shouldn't be displayed.
+
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews.size).isEqualTo(0)
+        verify(configurationController).removeCallback(any())
+    }
+
+    @Test
+    fun lowerThenHigherPriority_higherReplacesLower() {
+        underTest.displayView(
+            ViewInfo(
+                name = "normal",
+                windowTitle = "Normal Window Title",
+                id = "normal",
+                priority = ViewPriority.NORMAL,
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
+        reset(windowManager)
+
+        underTest.displayView(
+            ViewInfo(
+                name = "critical",
+                windowTitle = "Critical Window Title",
+                id = "critical",
+                priority = ViewPriority.CRITICAL,
+            )
+        )
+
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
+        verify(configurationController, never()).removeCallback(any())
+    }
+
+    @Test
+    fun lowerThenHigherPriority_lowerPriorityRedisplayed() {
+        underTest.displayView(
+            ViewInfo(
+                name = "normal",
+                windowTitle = "Normal Window Title",
+                id = "normal",
+                priority = ViewPriority.NORMAL,
+                timeoutMs = 10000
+            )
+        )
+
+        underTest.displayView(
+            ViewInfo(
+                name = "critical",
+                windowTitle = "Critical Window Title",
+                id = "critical",
+                priority = ViewPriority.CRITICAL,
+                timeoutMs = 2000
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager, times(2)).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("Normal Window Title")
+        assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Critical Window Title")
+        verify(windowManager).removeView(viewCaptor.allValues[0])
+
+        reset(windowManager)
+
+        // WHEN the critical's timeout has expired
+        fakeClock.advanceTime(2000 + 1)
+
+        // THEN the normal view is re-displayed
+        verify(windowManager).removeView(viewCaptor.allValues[1])
+        verify(windowManager).addView(any(), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
+        verify(configurationController, never()).removeCallback(any())
+    }
+
+    @Test
+    fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() {
+        underTest.displayView(
+            ViewInfo(
+                name = "normal",
+                windowTitle = "Normal Window Title",
+                id = "normal",
+                priority = ViewPriority.NORMAL,
+                timeoutMs = 1000
+            )
+        )
+
+        underTest.displayView(
+            ViewInfo(
+                name = "critical",
+                windowTitle = "Critical Window Title",
+                id = "critical",
+                priority = ViewPriority.CRITICAL,
+                timeoutMs = 2000
+            )
+        )
+        reset(windowManager)
+
+        // WHEN the critical's timeout has expired
+        fakeClock.advanceTime(2000 + 1)
+
+        // THEN the normal view is not re-displayed since it already timed out
+        verify(windowManager).removeView(any())
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews).isEmpty()
+        verify(configurationController).removeCallback(any())
+    }
+
+    @Test
+    fun higherThenLowerPriority_higherStaysDisplayed() {
+        underTest.displayView(
+            ViewInfo(
+                name = "critical",
+                windowTitle = "Critical Window Title",
+                id = "critical",
+                priority = ViewPriority.CRITICAL,
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
+        reset(windowManager)
+
+        underTest.displayView(
+            ViewInfo(
+                name = "normal",
+                windowTitle = "Normal Window Title",
+                id = "normal",
+                priority = ViewPriority.NORMAL,
+            )
+        )
+
+        verify(windowManager, never()).removeView(viewCaptor.value)
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        verify(configurationController, never()).removeCallback(any())
+    }
+
+    @Test
+    fun higherThenLowerPriority_lowerEventuallyDisplayed() {
+        underTest.displayView(
+            ViewInfo(
+                name = "critical",
+                windowTitle = "Critical Window Title",
+                id = "critical",
+                priority = ViewPriority.CRITICAL,
+                timeoutMs = 3000,
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
+        reset(windowManager)
+
+        underTest.displayView(
+            ViewInfo(
+                name = "normal",
+                windowTitle = "Normal Window Title",
+                id = "normal",
+                priority = ViewPriority.NORMAL,
+                timeoutMs = 5000,
+            )
+        )
+
+        verify(windowManager, never()).removeView(viewCaptor.value)
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+
+        // WHEN the first critical view has timed out
+        fakeClock.advanceTime(3000 + 1)
+
+        // THEN the second normal view is displayed
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
+        assertThat(underTest.activeViews.size).isEqualTo(1)
+        verify(configurationController, never()).removeCallback(any())
+    }
+
+    @Test
+    fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() {
+        underTest.displayView(
+            ViewInfo(
+                name = "critical",
+                windowTitle = "Critical Window Title",
+                id = "critical",
+                priority = ViewPriority.CRITICAL,
+                timeoutMs = 3000,
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
+        reset(windowManager)
+
+        underTest.displayView(
+            ViewInfo(
+                name = "normal",
+                windowTitle = "Normal Window Title",
+                id = "normal",
+                priority = ViewPriority.NORMAL,
+                timeoutMs = 200,
+            )
+        )
+
+        verify(windowManager, never()).removeView(viewCaptor.value)
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        reset(windowManager)
+
+        // WHEN the first critical view has timed out
+        fakeClock.advanceTime(3000 + 1)
+
+        // THEN the second normal view is not displayed because it's already timed out
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews).isEmpty()
+        verify(configurationController).removeCallback(any())
+    }
+
+    @Test
+    fun criticalThenNewCritical_newCriticalDisplayed() {
+        underTest.displayView(
+            ViewInfo(
+                name = "critical 1",
+                windowTitle = "Critical Window Title 1",
+                id = "critical1",
+                priority = ViewPriority.CRITICAL,
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 1")
+        reset(windowManager)
+
+        underTest.displayView(
+            ViewInfo(
+                name = "critical 2",
+                windowTitle = "Critical Window Title 2",
+                id = "critical2",
+                priority = ViewPriority.CRITICAL,
+            )
+        )
+
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2")
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        verify(configurationController, never()).removeCallback(any())
+    }
+
+    @Test
+    fun normalThenNewNormal_newNormalDisplayed() {
+        underTest.displayView(
+            ViewInfo(
+                name = "normal 1",
+                windowTitle = "Normal Window Title 1",
+                id = "normal1",
+                priority = ViewPriority.NORMAL,
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 1")
+        reset(windowManager)
+
+        underTest.displayView(
+            ViewInfo(
+                name = "normal 2",
+                windowTitle = "Normal Window Title 2",
+                id = "normal2",
+                priority = ViewPriority.NORMAL,
+            )
+        )
+
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2")
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        verify(configurationController, never()).removeCallback(any())
+    }
+
+    @Test
+    fun lowerPriorityViewUpdatedWhileHigherPriorityDisplayed_eventuallyDisplaysUpdated() {
+        // First, display a lower priority view
+        underTest.displayView(
+            ViewInfo(
+                name = "normal",
+                windowTitle = "Normal Window Title",
+                id = "normal",
+                priority = ViewPriority.NORMAL,
+                // At the end of the test, we'll verify that this information isn't re-displayed.
+                // Use a super long timeout so that, when we verify it wasn't re-displayed, we know
+                // that it wasn't because the view just timed out.
+                timeoutMs = 100000,
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
+        reset(windowManager)
+
+        // Then, display a higher priority view
+        fakeClock.advanceTime(1000)
+        underTest.displayView(
+            ViewInfo(
+                name = "critical",
+                windowTitle = "Critical Window Title",
+                id = "critical",
+                priority = ViewPriority.CRITICAL,
+                timeoutMs = 3000,
+            )
+        )
+
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        reset(windowManager)
+
+        // While the higher priority view is displayed, update the lower priority view with new
+        // information
+        fakeClock.advanceTime(1000)
+        val updatedViewInfo = ViewInfo(
+            name = "normal with update",
+            windowTitle = "Normal Window Title",
+            id = "normal",
+            priority = ViewPriority.NORMAL,
+            timeoutMs = 4000,
+        )
+        underTest.displayView(updatedViewInfo)
+
+        verify(windowManager, never()).removeView(viewCaptor.value)
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        reset(windowManager)
+
+        // WHEN the higher priority view times out
+        fakeClock.advanceTime(2001)
+
+        // THEN the higher priority view disappears and the lower priority view *with the updated
+        // information* gets displayed.
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
+        assertThat(underTest.activeViews.size).isEqualTo(1)
+        assertThat(underTest.mostRecentViewInfo).isEqualTo(updatedViewInfo)
+        reset(windowManager)
+
+        // WHEN the updated view times out
+        fakeClock.advanceTime(2001)
+
+        // THEN the old information is never displayed
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews.size).isEqualTo(0)
+    }
+
+    @Test
+    fun oldViewUpdatedWhileNewViewDisplayed_eventuallyDisplaysUpdated() {
+        // First, display id1 view
+        underTest.displayView(
+            ViewInfo(
+                name = "name 1",
+                windowTitle = "Name 1 Title",
+                id = "id1",
+                priority = ViewPriority.NORMAL,
+                // At the end of the test, we'll verify that this information isn't re-displayed.
+                // Use a super long timeout so that, when we verify it wasn't re-displayed, we know
+                // that it wasn't because the view just timed out.
+                timeoutMs = 100000,
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title")
+        reset(windowManager)
+
+        // Then, display a new id2 view
+        fakeClock.advanceTime(1000)
+        underTest.displayView(
+            ViewInfo(
+                name = "name 2",
+                windowTitle = "Name 2 Title",
+                id = "id2",
+                priority = ViewPriority.NORMAL,
+                timeoutMs = 3000,
+            )
+        )
+
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Name 2 Title")
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        reset(windowManager)
+
+        // While the id2 view is displayed, re-display the id1 view with new information
+        fakeClock.advanceTime(1000)
+        val updatedViewInfo = ViewInfo(
+            name = "name 1 with update",
+            windowTitle = "Name 1 Title",
+            id = "id1",
+            priority = ViewPriority.NORMAL,
+            timeoutMs = 3000,
+        )
+        underTest.displayView(updatedViewInfo)
+
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title")
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        reset(windowManager)
+
+        // WHEN the id1 view with new information times out
+        fakeClock.advanceTime(3001)
+
+        // THEN the id1 view disappears and the old id1 information is never displayed
+        verify(windowManager).removeView(viewCaptor.value)
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews.size).isEqualTo(0)
+    }
+
+    @Test
+    fun oldViewUpdatedWhileNewViewDisplayed_usesNewTimeout() {
+        // First, display id1 view
+        underTest.displayView(
+            ViewInfo(
+                name = "name 1",
+                windowTitle = "Name 1 Title",
+                id = "id1",
+                priority = ViewPriority.NORMAL,
+                timeoutMs = 5000,
+            )
+        )
+
+        // Then, display a new id2 view
+        fakeClock.advanceTime(1000)
+        underTest.displayView(
+            ViewInfo(
+                name = "name 2",
+                windowTitle = "Name 2 Title",
+                id = "id2",
+                priority = ViewPriority.NORMAL,
+                timeoutMs = 3000,
+            )
+        )
+        reset(windowManager)
+
+        // While the id2 view is displayed, re-display the id1 view with new information *and a
+        // longer timeout*
+        fakeClock.advanceTime(1000)
+        val updatedViewInfo = ViewInfo(
+            name = "name 1 with update",
+            windowTitle = "Name 1 Title",
+            id = "id1",
+            priority = ViewPriority.NORMAL,
+            timeoutMs = 30000,
+        )
+        underTest.displayView(updatedViewInfo)
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title")
+        assertThat(underTest.activeViews.size).isEqualTo(2)
+        reset(windowManager)
+
+        // WHEN id1's *old* timeout occurs
+        fakeClock.advanceTime(3001)
+
+        // THEN id1 is still displayed because it was updated with a new timeout
+        verify(windowManager, never()).removeView(viewCaptor.value)
+        assertThat(underTest.activeViews.size).isEqualTo(1)
     }
 
     @Test
@@ -395,6 +964,7 @@
 
         verify(windowManager).removeView(any())
         verify(logger).logViewRemoval(deviceId, reason)
+        verify(configurationController).removeCallback(any())
     }
 
     @Test
@@ -414,14 +984,15 @@
 
     inner class TestController(
         context: Context,
-        logger: TemporaryViewLogger,
+        logger: TemporaryViewLogger<ViewInfo>,
         windowManager: WindowManager,
         @Main mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
         powerManager: PowerManager,
         wakeLockBuilder: WakeLock.Builder,
-    ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger>(
+        systemClock: SystemClock,
+    ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger<ViewInfo>>(
         context,
         logger,
         windowManager,
@@ -431,6 +1002,7 @@
         powerManager,
         R.layout.chipbar,
         wakeLockBuilder,
+        systemClock,
     ) {
         var mostRecentViewInfo: ViewInfo? = null
 
@@ -447,12 +1019,13 @@
         override fun start() {}
     }
 
-    inner class ViewInfo(
+    data class ViewInfo(
         val name: String,
         override val windowTitle: String = "Window Title",
         override val wakeReason: String = "WAKE_REASON",
-        override val timeoutMs: Int = 1,
+        override val timeoutMs: Int = TIMEOUT_MS.toInt(),
         override val id: String = "id",
+        override val priority: ViewPriority = ViewPriority.NORMAL,
     ) : TemporaryViewInfo()
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
index 116b8fe..2e66b20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
@@ -32,7 +32,7 @@
 @SmallTest
 class TemporaryViewLoggerTest : SysuiTestCase() {
     private lateinit var buffer: LogBuffer
-    private lateinit var logger: TemporaryViewLogger
+    private lateinit var logger: TemporaryViewLogger<TemporaryViewInfo>
 
     @Before
     fun setUp() {
@@ -44,13 +44,22 @@
 
     @Test
     fun logViewAddition_bufferHasLog() {
-        logger.logViewAddition("test id", "Test Window Title")
+        val info =
+            object : TemporaryViewInfo() {
+                override val id: String = "test id"
+                override val priority: ViewPriority = ViewPriority.CRITICAL
+                override val windowTitle: String = "Test Window Title"
+                override val wakeReason: String = "wake reason"
+            }
+
+        logger.logViewAddition(info)
 
         val stringWriter = StringWriter()
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
         val actualString = stringWriter.toString()
 
         assertThat(actualString).contains(TAG)
+        assertThat(actualString).contains("test id")
         assertThat(actualString).contains("Test Window Title")
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 7014f93..2e4d8e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.temporarydisplay.ViewPriority
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -105,6 +106,7 @@
                 viewUtil,
                 vibratorHelper,
                 fakeWakeLockBuilder,
+                fakeClock,
             )
         underTest.start()
     }
@@ -408,6 +410,7 @@
             wakeReason = WAKE_REASON,
             timeoutMs = TIMEOUT,
             id = DEVICE_ID,
+            priority = ViewPriority.NORMAL,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
index beedf9f..d5167b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.android.systemui.util.wakelock.WakeLock
 
@@ -43,6 +44,7 @@
     viewUtil: ViewUtil,
     vibratorHelper: VibratorHelper,
     wakeLockBuilder: WakeLock.Builder,
+    systemClock: SystemClock,
 ) :
     ChipbarCoordinator(
         context,
@@ -57,6 +59,7 @@
         viewUtil,
         vibratorHelper,
         wakeLockBuilder,
+        systemClock,
     ) {
     override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java
index 14b9bfb..a707222 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java
@@ -26,8 +26,8 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.testing.AndroidTestingRunner;
+import android.view.AttachedSurfaceControl;
 import android.view.View;
-import android.view.ViewRootImpl;
 
 import androidx.test.filters.SmallTest;
 
@@ -47,41 +47,78 @@
 @RunWith(AndroidTestingRunner.class)
 public class TouchInsetManagerTest extends SysuiTestCase {
     @Mock
-    private View mRootView;
-
-    @Mock
-    private ViewRootImpl mRootViewImpl;
+    private AttachedSurfaceControl mAttachedSurfaceControl;
 
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        when(mRootView.getViewRootImpl()).thenReturn(mRootViewImpl);
     }
 
     @Test
-    public void testRootViewOnAttachedHandling() {
+    public void testViewOnAttachedHandling() {
         // Create inset manager
-        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
-                mRootView);
+        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
 
         final ArgumentCaptor<View.OnAttachStateChangeListener> listener =
                 ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+        final View view = createView(new Rect(0, 0, 0, 0));
+        when(view.isAttachedToWindow()).thenReturn(false);
 
+
+        // Create session
+        final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
+        session.addViewToTracking(view);
+
+        mFakeExecutor.runAllReady();
         // Ensure manager has registered to listen to attached state of root view.
-        verify(mRootView).addOnAttachStateChangeListener(listener.capture());
+        verify(view).addOnAttachStateChangeListener(listener.capture());
+
+        clearInvocations(mAttachedSurfaceControl);
+        when(view.isAttachedToWindow()).thenReturn(true);
 
         // Trigger attachment and verify touchable region is set.
-        listener.getValue().onViewAttachedToWindow(mRootView);
-        verify(mRootViewImpl).setTouchableRegion(any());
+        listener.getValue().onViewAttachedToWindow(view);
+
+        mFakeExecutor.runAllReady();
+
+        verify(mAttachedSurfaceControl).setTouchableRegion(any());
+    }
+
+    @Test
+    public void testViewOnDetachedHandling() {
+        // Create inset manager
+        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
+
+        final ArgumentCaptor<View.OnAttachStateChangeListener> listener =
+                ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+        final View view = createView(new Rect(0, 0, 0, 0));
+        when(view.isAttachedToWindow()).thenReturn(true);
+
+        // Create session
+        final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
+        session.addViewToTracking(view);
+
+        mFakeExecutor.runAllReady();
+        // Ensure manager has registered to listen to attached state of root view.
+        verify(view).addOnAttachStateChangeListener(listener.capture());
+
+        clearInvocations(mAttachedSurfaceControl);
+        when(view.isAttachedToWindow()).thenReturn(false);
+
+        // Trigger detachment and verify touchable region is set.
+        listener.getValue().onViewDetachedFromWindow(view);
+
+        mFakeExecutor.runAllReady();
+
+        verify(mAttachedSurfaceControl).setTouchableRegion(any());
     }
 
     @Test
     public void testInsetRegionPropagation() {
         // Create inset manager
-        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
-                mRootView);
+        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
 
         // Create session
         final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
@@ -95,14 +132,13 @@
         // Check to see if view was properly accounted for.
         final Region expectedRegion = Region.obtain();
         expectedRegion.op(rect, Region.Op.UNION);
-        verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+        verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
     }
 
     @Test
     public void testMultipleRegions() {
         // Create inset manager
-        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
-                mRootView);
+        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
 
         // Create session
         final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
@@ -112,7 +148,7 @@
         session.addViewToTracking(createView(firstBounds));
 
         mFakeExecutor.runAllReady();
-        clearInvocations(mRootViewImpl);
+        clearInvocations(mAttachedSurfaceControl);
 
         // Create second session
         final TouchInsetManager.TouchInsetSession secondSession = insetManager.createSession();
@@ -128,27 +164,26 @@
             final Region expectedRegion = Region.obtain();
             expectedRegion.op(firstBounds, Region.Op.UNION);
             expectedRegion.op(secondBounds, Region.Op.UNION);
-            verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+            verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
         }
 
 
-        clearInvocations(mRootViewImpl);
+        clearInvocations(mAttachedSurfaceControl);
 
         // clear first session, ensure second session is still reflected.
         session.clear();
         mFakeExecutor.runAllReady();
         {
             final Region expectedRegion = Region.obtain();
-            expectedRegion.op(firstBounds, Region.Op.UNION);
-            verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+            expectedRegion.op(secondBounds, Region.Op.UNION);
+            verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
         }
     }
 
     @Test
     public void testMultipleViews() {
         // Create inset manager
-        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
-                mRootView);
+        final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
 
         // Create session
         final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
@@ -159,7 +194,7 @@
 
         // only capture second invocation.
         mFakeExecutor.runAllReady();
-        clearInvocations(mRootViewImpl);
+        clearInvocations(mAttachedSurfaceControl);
 
         // Add a second view to the session
         final Rect secondViewBounds = new Rect(4, 4, 9, 10);
@@ -173,20 +208,20 @@
             final Region expectedRegion = Region.obtain();
             expectedRegion.op(firstViewBounds, Region.Op.UNION);
             expectedRegion.op(secondViewBounds, Region.Op.UNION);
-            verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+            verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
         }
 
         // Remove second view.
         session.removeViewFromTracking(secondView);
 
-        clearInvocations(mRootViewImpl);
+        clearInvocations(mAttachedSurfaceControl);
         mFakeExecutor.runAllReady();
 
         // Ensure first view still reflected in touch region.
         {
             final Region expectedRegion = Region.obtain();
             expectedRegion.op(firstViewBounds, Region.Op.UNION);
-            verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+            verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
         }
     }
 
@@ -197,6 +232,8 @@
             ((Rect) invocation.getArgument(0)).set(rect);
             return null;
         }).when(view).getBoundsOnScreen(any());
+        when(view.isAttachedToWindow()).thenReturn(true);
+        when(view.getRootSurfaceControl()).thenReturn(mAttachedSurfaceControl);
 
         return view;
     }
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 2ad2119..dd9f1d8 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -48,6 +48,7 @@
 import android.hardware.camera2.extension.IRequestProcessorImpl;
 import android.hardware.camera2.extension.IRequestUpdateProcessorImpl;
 import android.hardware.camera2.extension.ISessionProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
 import android.hardware.camera2.extension.LatencyRange;
 import android.hardware.camera2.extension.OutputConfigId;
 import android.hardware.camera2.extension.OutputSurface;
@@ -1266,6 +1267,21 @@
         public int startCapture(ICaptureCallback callback) {
             return mSessionProcessor.startCapture(new CaptureCallbackStub(callback, mCameraId));
         }
+
+        @Override
+        public LatencyPair getRealtimeCaptureLatency() {
+            if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+                Pair<Long, Long> latency = mSessionProcessor.getRealtimeCaptureLatency();
+                if (latency != null) {
+                    LatencyPair ret = new LatencyPair();
+                    ret.first = latency.first;
+                    ret.second = latency.second;
+                    return ret;
+                }
+            }
+
+            return null;
+        }
     }
 
     private class OutputSurfaceConfigurationImplStub implements OutputSurfaceConfigurationImpl {
@@ -1578,6 +1594,21 @@
         }
 
         @Override
+        public LatencyPair getRealtimeCaptureLatency() {
+            if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+                Pair<Long, Long> latency = mImageExtender.getRealtimeCaptureLatency();
+                if (latency != null) {
+                    LatencyPair ret = new LatencyPair();
+                    ret.first = latency.first;
+                    ret.second = latency.second;
+                    return ret;
+                }
+            }
+
+            return null;
+        }
+
+        @Override
         public CameraMetadataNative getAvailableCaptureRequestKeys() {
             if (RESULT_API_SUPPORTED) {
                 List<CaptureRequest.Key> supportedCaptureKeys =
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 3145139..59c1c54 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -26,8 +26,11 @@
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MAGNIFICATION_CONNECTION;
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL;
 import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
+import static android.provider.Settings.Secure.CONTRAST_LEVEL;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+import static android.view.accessibility.AccessibilityManager.CONTRAST_DEFAULT_VALUE;
+import static android.view.accessibility.AccessibilityManager.CONTRAST_NOT_SET;
 import static android.view.accessibility.AccessibilityManager.ShortcutType;
 
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
@@ -1018,10 +1021,13 @@
     }
 
     private void dispatchAccessibilityEventLocked(AccessibilityEvent event) {
-        notifyAccessibilityServicesDelayedLocked(event, false);
-        notifyAccessibilityServicesDelayedLocked(event, true);
+        if (mProxyManager.isProxyed(event.getDisplayId())) {
+            mProxyManager.sendAccessibilityEventLocked(event);
+        } else {
+            notifyAccessibilityServicesDelayedLocked(event, false);
+            notifyAccessibilityServicesDelayedLocked(event, true);
+        }
         mUiAutomationManager.sendAccessibilityEventLocked(event);
-        mProxyManager.sendAccessibilityEvent(event);
     }
 
     private void sendAccessibilityEventToInputFilter(AccessibilityEvent event) {
@@ -1162,7 +1168,7 @@
             }
             List<AccessibilityServiceConnection> services =
                     getUserStateLocked(resolvedUserId).mBoundServices;
-            int numServices = services.size() + mProxyManager.getNumProxys();
+            int numServices = services.size() + mProxyManager.getNumProxysLocked();
             interfacesToInterrupt = new ArrayList<>(numServices);
             for (int i = 0; i < services.size(); i++) {
                 AccessibilityServiceConnection service = services.get(i);
@@ -1172,7 +1178,7 @@
                     interfacesToInterrupt.add(a11yServiceInterface);
                 }
             }
-            mProxyManager.addServiceInterfaces(interfacesToInterrupt);
+            mProxyManager.addServiceInterfacesLocked(interfacesToInterrupt);
         }
         for (int i = 0, count = interfacesToInterrupt.size(); i < count; i++) {
             try {
@@ -1899,6 +1905,16 @@
         return false;
     }
 
+    private boolean readUiContrastLocked(AccessibilityUserState userState) {
+        float contrast = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
+                CONTRAST_LEVEL, CONTRAST_DEFAULT_VALUE, userState.mUserId);
+        if (Math.abs(userState.getUiContrastLocked() - contrast) >= 1e-10) {
+            userState.setUiContrastLocked(contrast);
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Performs {@link AccessibilityService}s delayed notification. The delay is configurable
      * and denotes the period after the last event before notifying the service.
@@ -1963,7 +1979,7 @@
                 mUiAutomationManager.getServiceInfo(), client)
                 ? mUiAutomationManager.getRelevantEventTypes()
                 : 0;
-        relevantEventTypes |= mProxyManager.getRelevantEventTypes();
+        relevantEventTypes |= mProxyManager.getRelevantEventTypesLocked();
         return relevantEventTypes;
     }
 
@@ -2565,6 +2581,7 @@
         somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState);
         somethingChanged |= readMagnificationCapabilitiesLocked(userState);
         somethingChanged |= readMagnificationFollowTypingLocked(userState);
+        somethingChanged |= readUiContrastLocked(userState);
         return somethingChanged;
     }
 
@@ -3706,6 +3723,19 @@
         return mProxyManager.unregisterProxy(displayId);
     }
 
+    @Override public float getUiContrast() {
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) {
+            mTraceManager.logTrace(LOG_TAG + ".getUiContrast", FLAGS_ACCESSIBILITY_MANAGER);
+        }
+        synchronized (mLock) {
+            AccessibilityUserState userState = getCurrentUserStateLocked();
+            float contrast = userState.getUiContrastLocked();
+            if (contrast != CONTRAST_NOT_SET) return contrast;
+            readUiContrastLocked(userState);
+            return userState.getUiContrastLocked();
+        }
+    }
+
     @Override
     public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
@@ -4153,6 +4183,9 @@
         private final Uri mMagnificationFollowTypingUri = Settings.Secure.getUriFor(
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED);
 
+        private final Uri mUiContrastUri = Settings.Secure.getUriFor(
+                CONTRAST_LEVEL);
+
         public AccessibilityContentObserver(Handler handler) {
             super(handler);
         }
@@ -4193,6 +4226,8 @@
                     mMagnificationCapabilityUri, false, this, UserHandle.USER_ALL);
             contentResolver.registerContentObserver(
                     mMagnificationFollowTypingUri, false, this, UserHandle.USER_ALL);
+            contentResolver.registerContentObserver(
+                    mUiContrastUri, false, this, UserHandle.USER_ALL);
         }
 
         @Override
@@ -4262,6 +4297,10 @@
                     }
                 } else if (mMagnificationFollowTypingUri.equals(uri)) {
                     readMagnificationFollowTypingLocked(userState);
+                } else if (mUiContrastUri.equals(uri)) {
+                    if (readUiContrastLocked(userState)) {
+                        updateUiContrastLocked(userState);
+                    }
                 }
             }
         }
@@ -4551,7 +4590,22 @@
                         userState.getFocusColorLocked());
             }));
         });
+    }
 
+    private void updateUiContrastLocked(AccessibilityUserState userState) {
+        if (userState.mUserId != mCurrentUserId) {
+            return;
+        }
+        if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
+            mTraceManager.logTrace(LOG_TAG + ".updateUiContrastLocked",
+                    FLAGS_ACCESSIBILITY_SERVICE_CLIENT, "userState=" + userState);
+        }
+        float contrast = userState.getUiContrastLocked();
+        mMainHandler.post(() -> {
+            broadcastToClients(userState, ignoreRemoteException(client -> {
+                client.mCallback.setUiContrast(contrast);
+            }));
+        });
     }
 
     public AccessibilityTraceManager getTraceManager() {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 0db169f..43730fc 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -26,6 +26,8 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
+import static android.view.accessibility.AccessibilityManager.CONTRAST_DEFAULT_VALUE;
+import static android.view.accessibility.AccessibilityManager.CONTRAST_NOT_SET;
 import static android.view.accessibility.AccessibilityManager.ShortcutType;
 
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -143,6 +145,8 @@
     private final int mFocusStrokeWidthDefaultValue;
     // The default value of the focus color.
     private final int mFocusColorDefaultValue;
+    /** The color contrast in [-1, 1] */
+    private float mUiContrast = CONTRAST_DEFAULT_VALUE;
 
     private Context mContext;
 
@@ -217,6 +221,7 @@
         mFocusStrokeWidth = mFocusStrokeWidthDefaultValue;
         mFocusColor = mFocusColorDefaultValue;
         mMagnificationFollowTypingEnabled = true;
+        mUiContrast = CONTRAST_NOT_SET;
     }
 
     void addServiceLocked(AccessibilityServiceConnection serviceConnection) {
@@ -983,6 +988,7 @@
         return mFocusColor;
     }
 
+
     /**
      * Sets the stroke width and color of the focus rectangle.
      *
@@ -1008,4 +1014,20 @@
         }
         return false;
     }
+
+    /**
+     * Get the color contrast
+     * @return color contrast in [-1, 1]
+     */
+    public float getUiContrastLocked() {
+        return mUiContrast;
+    }
+
+    /**
+     * Set the color contrast
+     * @param contrast the new color contrast in [-1, 1]
+     */
+    public void setUiContrastLocked(float contrast) {
+        mUiContrast = contrast;
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index 2184878..f28191f 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -37,6 +37,7 @@
  * proxy connection will belong to a separate user state.
  *
  * TODO(241117292): Remove or cut down during simultaneous user refactoring.
+ * TODO(262244375): Add unit tests.
  */
 public class ProxyManager {
     // Names used to populate ComponentName and ResolveInfo in connection.mA11yServiceInfo and in
@@ -84,7 +85,9 @@
                         windowManagerInternal,
                         awm, displayId);
 
-        mProxyA11yServiceConnections.put(displayId, connection);
+        synchronized (mLock) {
+            mProxyA11yServiceConnections.put(displayId, connection);
+        }
 
         // If the client dies, make sure to remove the connection.
         IBinder.DeathRecipient deathRecipient =
@@ -98,7 +101,9 @@
         client.asBinder().linkToDeath(deathRecipient, 0);
         // Notify apps that the service state has changed.
         // A11yManager#A11yServicesStateChangeListener
-        connection.mSystemSupport.onClientChangeLocked(true);
+        synchronized (mLock) {
+            connection.mSystemSupport.onClientChangeLocked(true);
+        }
 
         connection.initializeServiceInterface(client);
     }
@@ -111,9 +116,11 @@
     }
 
     private boolean clearConnection(int displayId) {
-        if (mProxyA11yServiceConnections.contains(displayId)) {
-            mProxyA11yServiceConnections.remove(displayId);
-            return true;
+        synchronized (mLock) {
+            if (mProxyA11yServiceConnections.contains(displayId)) {
+                mProxyA11yServiceConnections.remove(displayId);
+                return true;
+            }
         }
         return false;
     }
@@ -122,7 +129,9 @@
      * Checks if a display id is being proxy-ed.
      */
     public boolean isProxyed(int displayId) {
-        return mProxyA11yServiceConnections.contains(displayId);
+        synchronized (mLock) {
+            return mProxyA11yServiceConnections.contains(displayId);
+        }
     }
 
     /**
@@ -130,10 +139,10 @@
      * {@link android.view.accessibility.AccessibilityDisplayProxy} will filter based on display.
      * TODO(b/250929565): Filtering should happen in the system, not in the proxy.
      */
-    public void sendAccessibilityEvent(AccessibilityEvent event) {
-        for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) {
-            ProxyAccessibilityServiceConnection proxy =
-                    mProxyA11yServiceConnections.valueAt(i);
+    public void sendAccessibilityEventLocked(AccessibilityEvent event) {
+        final ProxyAccessibilityServiceConnection proxy =
+                mProxyA11yServiceConnections.get(event.getDisplayId());
+        if (proxy != null) {
             proxy.notifyAccessibilityEvent(event);
         }
     }
@@ -187,7 +196,7 @@
      * Returns the relevant event types of every proxy.
      * TODO(254545943): When A11yManager is separated, return based on the A11yManager display.
      */
-    public int getRelevantEventTypes() {
+    public int getRelevantEventTypesLocked() {
         int relevantEventTypes = 0;
         for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) {
             ProxyAccessibilityServiceConnection proxy =
@@ -201,7 +210,7 @@
      * Gets the number of current proxy connections.
      * @return
      */
-    public int getNumProxys() {
+    public int getNumProxysLocked() {
         return mProxyA11yServiceConnections.size();
     }
 
@@ -209,7 +218,7 @@
      * Adds the service interfaces to a list.
      * @param interfaces
      */
-    public void addServiceInterfaces(List<IAccessibilityServiceClient> interfaces) {
+    public void addServiceInterfacesLocked(List<IAccessibilityServiceClient> interfaces) {
         for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) {
             final ProxyAccessibilityServiceConnection proxy =
                     mProxyA11yServiceConnections.valueAt(i);
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 0cea3d0..97b5d6d 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -61,15 +61,19 @@
 
     private static final AtomicLong sNextPhysId = new AtomicLong(1);
 
+    static final String NAVIGATION_TOUCHPAD_DEVICE_TYPE = "touchNavigation";
+
     static final String PHYS_TYPE_DPAD = "Dpad";
     static final String PHYS_TYPE_KEYBOARD = "Keyboard";
     static final String PHYS_TYPE_MOUSE = "Mouse";
     static final String PHYS_TYPE_TOUCHSCREEN = "Touchscreen";
+    static final String PHYS_TYPE_NAVIGATION_TOUCHPAD = "NavigationTouchpad";
     @StringDef(prefix = { "PHYS_TYPE_" }, value = {
             PHYS_TYPE_DPAD,
             PHYS_TYPE_KEYBOARD,
             PHYS_TYPE_MOUSE,
             PHYS_TYPE_TOUCHSCREEN,
+            PHYS_TYPE_NAVIGATION_TOUCHPAD,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface PhysType {
@@ -190,6 +194,28 @@
         }
     }
 
+    void createNavigationTouchpad(
+            @NonNull String deviceName,
+            int vendorId,
+            int productId,
+            @NonNull IBinder deviceToken,
+            int displayId,
+            int touchpadHeight,
+            int touchpadWidth) {
+        final String phys = createPhys(PHYS_TYPE_NAVIGATION_TOUCHPAD);
+        mInputManagerInternal.setTypeAssociation(phys, NAVIGATION_TOUCHPAD_DEVICE_TYPE);
+        try {
+            createDeviceInternal(InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD, deviceName,
+                    vendorId, productId, deviceToken, displayId, phys,
+                    () -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
+                            phys, touchpadHeight, touchpadWidth));
+        } catch (DeviceCreationException e) {
+            mInputManagerInternal.unsetTypeAssociation(phys);
+            throw new RuntimeException(
+                    "Failed to create virtual navigation touchpad device '" + deviceName + "'.", e);
+        }
+    }
+
     void unregisterInputDevice(@NonNull IBinder token) {
         synchronized (mLock) {
             final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove(
@@ -207,7 +233,13 @@
             InputDeviceDescriptor inputDeviceDescriptor) {
         token.unlinkToDeath(inputDeviceDescriptor.getDeathRecipient(), /* flags= */ 0);
         mNativeWrapper.closeUinput(inputDeviceDescriptor.getFileDescriptor());
+
         InputManager.getInstance().removeUniqueIdAssociation(inputDeviceDescriptor.getPhys());
+        // Type associations are added in the case of navigation touchpads. Those should be removed
+        // once the input device gets closed.
+        if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD) {
+            mInputManagerInternal.unsetTypeAssociation(inputDeviceDescriptor.getPhys());
+        }
 
         // Reset values to the default if all virtual mice are unregistered, or set display
         // id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be
@@ -509,11 +541,13 @@
         static final int TYPE_MOUSE = 2;
         static final int TYPE_TOUCHSCREEN = 3;
         static final int TYPE_DPAD = 4;
+        static final int TYPE_NAVIGATION_TOUCHPAD = 5;
         @IntDef(prefix = { "TYPE_" }, value = {
                 TYPE_KEYBOARD,
                 TYPE_MOUSE,
                 TYPE_TOUCHSCREEN,
                 TYPE_DPAD,
+                TYPE_NAVIGATION_TOUCHPAD,
         })
         @Retention(RetentionPolicy.SOURCE)
         @interface Type {
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 5819861..12ad9f1 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -53,6 +53,7 @@
 import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
 import android.hardware.input.VirtualTouchEvent;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.os.Binder;
@@ -108,7 +109,7 @@
     private VirtualAudioController mVirtualAudioController;
     @VisibleForTesting
     final Set<Integer> mVirtualDisplayIds = new ArraySet<>();
-    private final OnDeviceCloseListener mListener;
+    private final OnDeviceCloseListener mOnDeviceCloseListener;
     private final IBinder mAppToken;
     private final VirtualDeviceParams mParams;
     private final Map<Integer, PowerManager.WakeLock> mPerDisplayWakelocks = new ArrayMap<>();
@@ -155,7 +156,7 @@
             IBinder token,
             int ownerUid,
             int deviceId,
-            OnDeviceCloseListener listener,
+            OnDeviceCloseListener onDeviceCloseListener,
             PendingTrampolineCallback pendingTrampolineCallback,
             IVirtualDeviceActivityListener activityListener,
             Consumer<ArraySet<Integer>> runningAppsChangedCallback,
@@ -168,7 +169,7 @@
                 deviceId,
                 /* inputController= */ null,
                 /* sensorController= */ null,
-                listener,
+                onDeviceCloseListener,
                 pendingTrampolineCallback,
                 activityListener,
                 runningAppsChangedCallback,
@@ -184,7 +185,7 @@
             int deviceId,
             InputController inputController,
             SensorController sensorController,
-            OnDeviceCloseListener listener,
+            OnDeviceCloseListener onDeviceCloseListener,
             PendingTrampolineCallback pendingTrampolineCallback,
             IVirtualDeviceActivityListener activityListener,
             Consumer<ArraySet<Integer>> runningAppsChangedCallback,
@@ -212,7 +213,7 @@
         } else {
             mSensorController = sensorController;
         }
-        mListener = listener;
+        mOnDeviceCloseListener = onDeviceCloseListener;
         try {
             token.linkToDeath(this, 0);
         } catch (RemoteException e) {
@@ -330,7 +331,7 @@
                 mVirtualAudioController = null;
             }
         }
-        mListener.onClose(mAssociationInfo.getId());
+        mOnDeviceCloseListener.onClose(mDeviceId);
         mAppToken.unlinkToDeath(this, 0);
 
         final long ident = Binder.clearCallingIdentity();
@@ -491,6 +492,38 @@
     }
 
     @Override // Binder call
+    public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
+            @NonNull IBinder deviceToken) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                "Permission required to create a virtual navigation touchpad");
+        synchronized (mVirtualDeviceLock) {
+            if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
+                throw new SecurityException(
+                        "Cannot create a virtual navigation touchpad for a display not associated "
+                                + "with this virtual device");
+            }
+        }
+        int touchpadHeight = config.getHeight();
+        int touchpadWidth = config.getWidth();
+        if (touchpadHeight <= 0 || touchpadWidth <= 0) {
+            throw new IllegalArgumentException(
+                "Cannot create a virtual navigation touchpad, touchpad dimensions must be positive."
+                    + " Got: (" + touchpadHeight + ", " + touchpadWidth + ")");
+        }
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mInputController.createNavigationTouchpad(
+                    config.getInputDeviceName(), config.getVendorId(),
+                    config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
+                    touchpadHeight, touchpadWidth);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override // Binder call
     public void unregisterInputDevice(IBinder token) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
@@ -650,6 +683,7 @@
     @Override
     protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
         fout.println("  VirtualDevice: ");
+        fout.println("    mDeviceId: " + mDeviceId);
         fout.println("    mAssociationId: " + mAssociationInfo.getId());
         fout.println("    mParams: " + mParams);
         fout.println("    mVirtualDisplayIds: ");
@@ -839,7 +873,7 @@
     }
 
     interface OnDeviceCloseListener {
-        void onClose(int associationId);
+        void onClose(int deviceId);
     }
 
     interface PendingTrampolineCallback {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index e092f49..da2c516 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -41,6 +41,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Parcel;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.ArraySet;
@@ -62,6 +63,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -88,14 +90,20 @@
             new SparseArray<>();
 
     /**
-     * Mapping from CDM association IDs to virtual devices. Only one virtual device is allowed for
-     * each CDM associated device.
+     * Mapping from device IDs to CameraAccessControllers.
+     */
+    @GuardedBy("mVirtualDeviceManagerLock")
+    private final SparseArray<CameraAccessController> mCameraAccessControllersByDeviceId =
+            new SparseArray<>();
+
+    /**
+     * Mapping from device IDs to virtual devices.
      */
     @GuardedBy("mVirtualDeviceManagerLock")
     private final SparseArray<VirtualDeviceImpl> mVirtualDevices = new SparseArray<>();
 
     /**
-     * Mapping from CDM association IDs to app UIDs running on the corresponding virtual device.
+     * Mapping from device IDs to app UIDs running on the corresponding virtual device.
      */
     @GuardedBy("mVirtualDeviceManagerLock")
     private final SparseArray<ArraySet<Integer>> mAppsOnVirtualDevices = new SparseArray<>();
@@ -158,7 +166,7 @@
     @GuardedBy("mVirtualDeviceManagerLock")
     private boolean isValidVirtualDeviceLocked(IVirtualDevice virtualDevice) {
         try {
-            return mVirtualDevices.contains(virtualDevice.getAssociationId());
+            return mVirtualDevices.contains(virtualDevice.getDeviceId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -228,9 +236,9 @@
     }
 
     @VisibleForTesting
-    void notifyRunningAppsChanged(int associationId, ArraySet<Integer> uids) {
+    void notifyRunningAppsChanged(int deviceId, ArraySet<Integer> uids) {
         synchronized (mVirtualDeviceManagerLock) {
-            mAppsOnVirtualDevices.put(associationId, uids);
+            mAppsOnVirtualDevices.put(deviceId, uids);
         }
         mLocalService.onAppsOnVirtualDeviceChanged();
     }
@@ -238,7 +246,7 @@
     @VisibleForTesting
     void addVirtualDevice(VirtualDeviceImpl virtualDevice) {
         synchronized (mVirtualDeviceManagerLock) {
-            mVirtualDevices.put(virtualDevice.getAssociationId(), virtualDevice);
+            mVirtualDevices.put(virtualDevice.getDeviceId(), virtualDevice);
         }
     }
 
@@ -266,60 +274,26 @@
                 throw new IllegalArgumentException("No association with ID " + associationId);
             }
             synchronized (mVirtualDeviceManagerLock) {
-                if (mVirtualDevices.contains(associationId)) {
-                    throw new IllegalStateException(
-                            "Virtual device for association ID " + associationId
-                                    + " already exists");
-                }
                 final int userId = UserHandle.getUserId(callingUid);
                 final CameraAccessController cameraAccessController =
                         mCameraAccessControllers.get(userId);
-                final int uniqueId = sNextUniqueIndex.getAndIncrement();
-
+                final int deviceId = sNextUniqueIndex.getAndIncrement();
                 VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(),
-                        associationInfo, token, callingUid, uniqueId,
-                        new VirtualDeviceImpl.OnDeviceCloseListener() {
-                            @Override
-                            public void onClose(int associationId) {
-                                synchronized (mVirtualDeviceManagerLock) {
-                                    VirtualDeviceImpl removedDevice =
-                                            mVirtualDevices.removeReturnOld(associationId);
-                                    if (removedDevice != null) {
-                                        Intent i = new Intent(
-                                                VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED);
-                                        i.putExtra(
-                                                VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID,
-                                                removedDevice.getDeviceId());
-                                        i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-                                        final long identity = Binder.clearCallingIdentity();
-                                        try {
-                                            getContext().sendBroadcastAsUser(i, UserHandle.ALL);
-                                        } finally {
-                                            Binder.restoreCallingIdentity(identity);
-                                        }
-                                    }
-                                    mAppsOnVirtualDevices.remove(associationId);
-                                    if (cameraAccessController != null) {
-                                        cameraAccessController.stopObservingIfNeeded();
-                                    } else {
-                                        Slog.w(TAG, "cameraAccessController not found for user "
-                                                + userId);
-                                    }
-                                }
-                            }
-                        },
+                        associationInfo, token, callingUid, deviceId,
+                        /* onDeviceCloseListener= */ this::onDeviceClosed,
                         this, activityListener,
                         runningUids -> {
                             cameraAccessController.blockCameraAccessIfNeeded(runningUids);
-                            notifyRunningAppsChanged(associationInfo.getId(), runningUids);
+                            notifyRunningAppsChanged(deviceId, runningUids);
                         },
                         params);
                 if (cameraAccessController != null) {
                     cameraAccessController.startObservingIfNeeded();
+                    mCameraAccessControllersByDeviceId.put(deviceId, cameraAccessController);
                 } else {
                     Slog.w(TAG, "cameraAccessController not found for user " + userId);
                 }
-                mVirtualDevices.put(associationInfo.getId(), virtualDevice);
+                mVirtualDevices.put(deviceId, virtualDevice);
                 return virtualDevice;
             }
         }
@@ -336,7 +310,7 @@
             }
             VirtualDeviceImpl virtualDeviceImpl;
             synchronized (mVirtualDeviceManagerLock) {
-                virtualDeviceImpl = mVirtualDevices.get(virtualDevice.getAssociationId());
+                virtualDeviceImpl = mVirtualDevices.get(virtualDevice.getDeviceId());
                 if (virtualDeviceImpl == null) {
                     throw new SecurityException("Invalid VirtualDevice");
                 }
@@ -417,8 +391,7 @@
         @Nullable
         private AssociationInfo getAssociationInfo(String packageName, int associationId) {
             final int callingUserId = getCallingUserHandle().getIdentifier();
-            final List<AssociationInfo> associations =
-                    mAllAssociations.get(callingUserId);
+            final List<AssociationInfo> associations = mAllAssociations.get(callingUserId);
             if (associations != null) {
                 final int associationSize = associations.size();
                 for (int i = 0; i < associationSize; i++) {
@@ -434,6 +407,29 @@
             return null;
         }
 
+        private void onDeviceClosed(int deviceId) {
+            synchronized (mVirtualDeviceManagerLock) {
+                mVirtualDevices.remove(deviceId);
+                Intent i = new Intent(VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED);
+                i.putExtra(VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID, deviceId);
+                i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    getContext().sendBroadcastAsUser(i, UserHandle.ALL);
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+                mAppsOnVirtualDevices.remove(deviceId);
+                final CameraAccessController cameraAccessController =
+                        mCameraAccessControllersByDeviceId.removeReturnOld(deviceId);
+                if (cameraAccessController != null) {
+                    cameraAccessController.stopObservingIfNeeded();
+                } else {
+                    Slog.w(TAG, "cameraAccessController not found for device Id " + deviceId);
+                }
+            }
+        }
+
         @Override
         public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
                 throws RemoteException {
@@ -495,6 +491,35 @@
         }
 
         @Override
+        public int getDeviceOwnerUid(int deviceId) {
+            synchronized (mVirtualDeviceManagerLock) {
+                int size = mVirtualDevices.size();
+                for (int i = 0; i < size; i++) {
+                    VirtualDeviceImpl device = mVirtualDevices.valueAt(i);
+                    if (device.getDeviceId() == deviceId) {
+                        return device.getOwnerUid();
+                    }
+                }
+            }
+            return Process.INVALID_UID;
+        }
+
+        @Override
+        public @NonNull Set<Integer> getDeviceIdsForUid(int uid) {
+            ArraySet<Integer> result = new ArraySet<>();
+            synchronized (mVirtualDeviceManagerLock) {
+                int size = mVirtualDevices.size();
+                for (int i = 0; i < size; i++) {
+                    VirtualDeviceImpl device = mVirtualDevices.valueAt(i);
+                    if (device.isAppRunningOnVirtualDevice(uid)) {
+                        result.add(device.getDeviceId());
+                    }
+                }
+            }
+            return result;
+        }
+
+        @Override
         public void onVirtualDisplayCreated(int displayId) {
             final VirtualDisplayListener[] listeners;
             synchronized (mVirtualDeviceManagerLock) {
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index a79aa0b..17002d5 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -119,7 +119,7 @@
     // used for indicating newly installed MBAs that are updated (but unused currently)
     static final int MBA_STATUS_UPDATED_NEW_INSTALL = 4;
 
-    private static final boolean DEBUG = true;     // set this to false upon submission
+    private static final boolean DEBUG = false;     // toggle this for local debug
 
     private final Context mContext;
     private String mVbmetaDigest;
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index b2fc574..5eb0db1 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -36,6 +36,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.DumpUtils;
+import com.android.server.pm.UserManagerInternal;
 
 import libcore.io.IoUtils;
 
@@ -156,14 +157,19 @@
         mBlockDeviceSize = -1; // Load lazily
     }
 
-    private int getAllowedUid(int userHandle) {
+    private int getAllowedUid() {
+        final UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
+        final int mainUserId = umInternal.getMainUserId();
+        if (mainUserId < 0) {
+            return -1;
+        }
         String allowedPackage = mContext.getResources()
                 .getString(R.string.config_persistentDataPackageName);
         int allowedUid = -1;
         if (!TextUtils.isEmpty(allowedPackage)) {
             try {
                 allowedUid = mContext.getPackageManager().getPackageUidAsUser(
-                        allowedPackage, PackageManager.MATCH_SYSTEM_ONLY, userHandle);
+                        allowedPackage, PackageManager.MATCH_SYSTEM_ONLY, mainUserId);
             } catch (PackageManager.NameNotFoundException e) {
                 // not expected
                 Slog.e(TAG, "not able to find package " + allowedPackage, e);
@@ -176,7 +182,7 @@
     public void onStart() {
         // Do init on a separate thread, will join in PHASE_ACTIVITY_MANAGER_READY
         SystemServerInitThreadPool.submit(() -> {
-            mAllowedUid = getAllowedUid(UserHandle.USER_SYSTEM);
+            mAllowedUid = getAllowedUid();
             enforceChecksumValidity();
             formatIfOemUnlockEnabled();
             publishBinderService(Context.PERSISTENT_DATA_BLOCK_SERVICE, mService);
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 32afcca..ee922f9 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -92,6 +92,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
+import com.android.internal.telephony.ICarrierConfigChangeListener;
 import com.android.internal.telephony.ICarrierPrivilegesCallback;
 import com.android.internal.telephony.IOnSubscriptionsChangedListener;
 import com.android.internal.telephony.IPhoneStateListener;
@@ -155,6 +156,7 @@
         IOnSubscriptionsChangedListener onSubscriptionsChangedListenerCallback;
         IOnSubscriptionsChangedListener onOpportunisticSubscriptionsChangedListenerCallback;
         ICarrierPrivilegesCallback carrierPrivilegesCallback;
+        ICarrierConfigChangeListener carrierConfigChangeListener;
 
         int callerUid;
         int callerPid;
@@ -183,6 +185,10 @@
             return carrierPrivilegesCallback != null;
         }
 
+        boolean matchCarrierConfigChangeListener() {
+            return carrierConfigChangeListener != null;
+        }
+
         boolean canReadCallLog() {
             try {
                 return TelephonyPermissions.checkReadCallLog(
@@ -201,6 +207,7 @@
                     + " onOpportunisticSubscriptionsChangedListenererCallback="
                     + onOpportunisticSubscriptionsChangedListenerCallback
                     + " carrierPrivilegesCallback=" + carrierPrivilegesCallback
+                    + " carrierConfigChangeListener=" + carrierConfigChangeListener
                     + " subId=" + subId + " phoneId=" + phoneId + " events=" + eventList + "}";
         }
     }
@@ -3044,6 +3051,82 @@
         }
     }
 
+    @Override
+    public void addCarrierConfigChangeListener(ICarrierConfigChangeListener listener,
+            String pkg, String featureId) {
+        final int callerUserId = UserHandle.getCallingUserId();
+        mAppOps.checkPackage(Binder.getCallingUid(), pkg);
+        if (VDBG) {
+            log("addCarrierConfigChangeListener pkg=" + pii(pkg) + " uid=" + Binder.getCallingUid()
+                    + " myUserId=" + UserHandle.myUserId() + " callerUerId" + callerUserId
+                    + " listener=" + listener + " listener.asBinder=" + listener.asBinder());
+        }
+
+        synchronized (mRecords) {
+            IBinder b = listener.asBinder();
+            boolean doesLimitApply = doesLimitApplyForListeners(Binder.getCallingUid(),
+                    Process.myUid());
+            Record r = add(b, Binder.getCallingUid(), Binder.getCallingPid(), doesLimitApply);
+
+            if (r == null) {
+                loge("Can not create Record instance!");
+                return;
+            }
+
+            r.context = mContext;
+            r.carrierConfigChangeListener = listener;
+            r.callingPackage = pkg;
+            r.callingFeatureId = featureId;
+            r.callerUid = Binder.getCallingUid();
+            r.callerPid = Binder.getCallingPid();
+            r.eventList = new ArraySet<>();
+            if (DBG) {
+                log("addCarrierConfigChangeListener:  Register r=" + r);
+            }
+        }
+    }
+
+    @Override
+    public void removeCarrierConfigChangeListener(ICarrierConfigChangeListener listener,
+            String pkg) {
+        if (DBG) log("removeCarrierConfigChangeListener listener=" + listener + ", pkg=" + pkg);
+        mAppOps.checkPackage(Binder.getCallingUid(), pkg);
+        remove(listener.asBinder());
+    }
+
+    @Override
+    public void notifyCarrierConfigChanged(int phoneId, int subId, int carrierId,
+            int specificCarrierId) {
+        if (!validatePhoneId(phoneId)) {
+            throw new IllegalArgumentException("Invalid phoneId: " + phoneId);
+        }
+        if (!checkNotifyPermission("notifyCarrierConfigChanged")) {
+            loge("Caller has no notify permission!");
+            return;
+        }
+        if (VDBG) {
+            log("notifyCarrierConfigChanged: phoneId=" + phoneId + ", subId=" + subId
+                    + ", carrierId=" + carrierId + ", specificCarrierId=" + specificCarrierId);
+        }
+
+        synchronized (mRecords) {
+            mRemoveList.clear();
+            for (Record r : mRecords) {
+                // Listeners are "global", neither per-slot nor per-sub, so no idMatch check here
+                if (!r.matchCarrierConfigChangeListener()) {
+                    continue;
+                }
+                try {
+                    r.carrierConfigChangeListener.onCarrierConfigChanged(phoneId, subId, carrierId,
+                            specificCarrierId);
+                } catch (RemoteException re) {
+                    mRemoveList.add(r.binder);
+                }
+            }
+            handleRemoveListLocked();
+        }
+    }
+
     @NeverCompile // Avoid size overhead of debugging code.
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 61f7f30..f652cb0 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -371,8 +371,9 @@
             return new LocationPermissionChecker(context);
         }
 
-        /** Gets the transports that need to be marked as restricted by the VCN */
-        public Set<Integer> getRestrictedTransports(
+        /** Gets transports that need to be marked as restricted by the VCN from CarrierConfig */
+        @VisibleForTesting(visibility = Visibility.PRIVATE)
+        public Set<Integer> getRestrictedTransportsFromCarrierConfig(
                 ParcelUuid subGrp, TelephonySubscriptionSnapshot lastSnapshot) {
             if (!Build.IS_ENG && !Build.IS_USERDEBUG) {
                 return RESTRICTED_TRANSPORTS_DEFAULT;
@@ -398,6 +399,22 @@
             }
             return restrictedTransports;
         }
+
+        /** Gets the transports that need to be marked as restricted by the VCN */
+        public Set<Integer> getRestrictedTransports(
+                ParcelUuid subGrp,
+                TelephonySubscriptionSnapshot lastSnapshot,
+                VcnConfig vcnConfig) {
+            final Set<Integer> restrictedTransports = new ArraySet<>();
+            restrictedTransports.addAll(vcnConfig.getRestrictedUnderlyingNetworkTransports());
+
+            // TODO: b/262269892 Remove the ability to configure restricted transports
+            // via CarrierConfig
+            restrictedTransports.addAll(
+                    getRestrictedTransportsFromCarrierConfig(subGrp, lastSnapshot));
+
+            return restrictedTransports;
+        }
     }
 
     /** Notifies the VcnManagementService that external dependencies can be set up. */
@@ -719,6 +736,7 @@
         if (mVcns.containsKey(subscriptionGroup)) {
             final Vcn vcn = mVcns.get(subscriptionGroup);
             vcn.updateConfig(config);
+            notifyAllPolicyListenersLocked();
         } else {
             // TODO(b/193687515): Support multiple VCNs active at the same time
             if (isActiveSubGroup(subscriptionGroup, mLastSnapshot)) {
@@ -936,7 +954,6 @@
     }
 
     /** Adds the provided listener for receiving VcnUnderlyingNetworkPolicy updates. */
-    @GuardedBy("mLock")
     @Override
     public void addVcnUnderlyingNetworkPolicyListener(
             @NonNull IVcnUnderlyingNetworkPolicyListener listener) {
@@ -963,16 +980,7 @@
         });
     }
 
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    void addVcnUnderlyingNetworkPolicyListenerForTest(
-            @NonNull IVcnUnderlyingNetworkPolicyListener listener) {
-        synchronized (mLock) {
-            addVcnUnderlyingNetworkPolicyListener(listener);
-        }
-    }
-
     /** Removes the provided listener from receiving VcnUnderlyingNetworkPolicy updates. */
-    @GuardedBy("mLock")
     @Override
     public void removeVcnUnderlyingNetworkPolicyListener(
             @NonNull IVcnUnderlyingNetworkPolicyListener listener) {
@@ -1062,8 +1070,8 @@
                         isVcnManagedNetwork = true;
                     }
 
-                    final Set<Integer> restrictedTransports =
-                            mDeps.getRestrictedTransports(subGrp, mLastSnapshot);
+                    final Set<Integer> restrictedTransports = mDeps.getRestrictedTransports(
+                            subGrp, mLastSnapshot, mConfigs.get(subGrp));
                     for (int restrictedTransport : restrictedTransports) {
                         if (ncCopy.hasTransport(restrictedTransport)) {
                             if (restrictedTransport == TRANSPORT_CELLULAR) {
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index a2755be..88492ed 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -3558,8 +3558,7 @@
             Bundle.setDefusable(result, true);
             mNumResults++;
             Intent intent = null;
-            if (result != null
-                    && (intent = result.getParcelable(AccountManager.KEY_INTENT, android.content.Intent.class)) != null) {
+            if (result != null) {
                 if (!checkKeyIntent(
                         Binder.getCallingUid(),
                         result)) {
@@ -4928,8 +4927,10 @@
                 EventLog.writeEvent(0x534e4554, "250588548", authUid, "");
                 return false;
             }
-
             Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
+            if (intent == null) {
+                return true;
+            }
             // Explicitly set an empty ClipData to ensure that we don't offer to
             // promote any Uris contained inside for granting purposes
             if (intent.getClipData() == null) {
@@ -4979,8 +4980,12 @@
             Bundle simulateBundle = p.readBundle();
             p.recycle();
             Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
-            return (intent.filterEquals(simulateBundle.getParcelable(AccountManager.KEY_INTENT,
-                Intent.class)));
+            Intent simulateIntent = simulateBundle.getParcelable(AccountManager.KEY_INTENT,
+                    Intent.class);
+            if (intent == null) {
+                return (simulateIntent == null);
+            }
+            return intent.filterEquals(simulateIntent);
         }
 
         private boolean isExportedSystemActivity(ActivityInfo activityInfo) {
@@ -5129,8 +5134,7 @@
                     }
                 }
             }
-            if (result != null
-                    && (intent = result.getParcelable(AccountManager.KEY_INTENT, android.content.Intent.class)) != null) {
+            if (result != null) {
                 if (!checkKeyIntent(
                         Binder.getCallingUid(),
                         result)) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 5a27af0..045c757 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -7438,11 +7438,10 @@
         if (shareDescription != null) {
             triggerShellBugreport.putExtra(EXTRA_DESCRIPTION, shareDescription);
         }
-        UserHandle callingUser = Binder.getCallingUserHandle();
         final long identity = Binder.clearCallingIdentity();
         try {
             // Send broadcast to shell to trigger bugreport using Bugreport API
-            mContext.sendBroadcastAsUser(triggerShellBugreport, callingUser);
+            mContext.sendBroadcastAsUser(triggerShellBugreport, UserHandle.SYSTEM);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
@@ -13578,9 +13577,10 @@
             // updating their receivers to be exempt from this requirement until their receivers
             // are flagged.
             if (requireExplicitFlagForDynamicReceivers) {
-                if ("com.google.android.apps.messaging".equals(callerPackage)) {
-                    // Note, a versionCode check for this package is not performed because it could
-                    // cause breakage with a subsequent update outside the system image.
+                if ("com.shannon.imsservice".equals(callerPackage)) {
+                    // Note, a versionCode check for this package is not performed because this
+                    // package consumes the SecurityException, so it wouldn't be caught during
+                    // presubmit.
                     requireExplicitFlagForDynamicReceivers = false;
                 }
             }
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 94c15ba..4a6e5a3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -1413,6 +1413,7 @@
                 mPw.println("shortMsg: " + shortMsg);
                 mPw.println("longMsg: " + longMsg);
                 mPw.println("timeMillis: " + timeMillis);
+                mPw.println("uptime: " + SystemClock.uptimeMillis());
                 mPw.println("stack:");
                 mPw.print(stackTrace);
                 mPw.println("#");
@@ -1429,6 +1430,7 @@
                 mPw.println("processName: " + processName);
                 mPw.println("processPid: " + pid);
                 mPw.println("annotation: " + annotation);
+                mPw.println("uptime: " + SystemClock.uptimeMillis());
                 mPw.flush();
                 int result = waitControllerLocked(pid, STATE_EARLY_ANR);
                 if (result == RESULT_EARLY_ANR_KILL) return -1;
@@ -1442,6 +1444,7 @@
                 mPw.println("** ERROR: PROCESS NOT RESPONDING");
                 mPw.println("processName: " + processName);
                 mPw.println("processPid: " + pid);
+                mPw.println("uptime: " + SystemClock.uptimeMillis());
                 mPw.println("processStats:");
                 mPw.print(processStats);
                 mPw.println("#");
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 7013df1..fceefad 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -532,15 +532,13 @@
     }
 
     public void traceActiveBegin() {
-        final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
         Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                runningTraceTrackName, mActive.toShortString() + " scheduled", cookie);
+                runningTraceTrackName, mActive.toShortString() + " scheduled", hashCode());
     }
 
     public void traceActiveEnd() {
-        final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
         Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                runningTraceTrackName, cookie);
+                runningTraceTrackName, hashCode());
     }
 
     /**
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 280256f..e5123ef 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -1571,12 +1571,6 @@
         checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay",
                 MANAGE_USERS, INTERACT_ACROSS_USERS);
 
-        // DEFAULT_DISPLAY is used for the current foreground user only
-        // TODO(b/245939659): might need to move this check to UserVisibilityMediator to support
-        // passenger-only screens
-        Preconditions.checkArgument(displayId != Display.DEFAULT_DISPLAY,
-                "Cannot use DEFAULT_DISPLAY");
-
         try {
             return startUserNoChecks(userId, displayId, USER_START_MODE_BACKGROUND_VISIBLE,
                     /* unlockListener= */ null);
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
index 587fb04..ac25f4e 100644
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java
@@ -20,7 +20,7 @@
 import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES;
 import static android.app.AppOpsManager.opRestrictsRead;
 
-import static com.android.server.appop.AppOpsServiceImpl.ModeCallback.ALL_OPS;
+import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -85,8 +85,8 @@
 
 
     AppOpsCheckingServiceImpl(PersistenceScheduler persistenceScheduler,
-                              @NonNull Object lock, Handler handler, Context context,
-                              SparseArray<int[]> switchedOps) {
+            @NonNull Object lock, Handler handler, Context context,
+            SparseArray<int[]> switchedOps) {
         this.mPersistenceScheduler = persistenceScheduler;
         this.mLock = lock;
         this.mHandler = handler;
@@ -218,7 +218,7 @@
     }
 
     @Override
-    public boolean arePackageModesDefault(@NonNull String packageMode, @UserIdInt int userId) {
+    public boolean arePackageModesDefault(String packageMode, @UserIdInt int userId) {
         synchronized (mLock) {
             ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
             if (packageModes == null) {
@@ -490,16 +490,15 @@
     }
 
     @Override
-    public SparseBooleanArray evalForegroundUidOps(int uid,
-            @Nullable SparseBooleanArray foregroundOps) {
+    public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) {
         synchronized (mLock) {
             return evalForegroundOps(mUidModes.get(uid), foregroundOps);
         }
     }
 
     @Override
-    public SparseBooleanArray evalForegroundPackageOps(@NonNull String packageName,
-            @Nullable SparseBooleanArray foregroundOps, @UserIdInt int userId) {
+    public SparseBooleanArray evalForegroundPackageOps(String packageName,
+            SparseBooleanArray foregroundOps, @UserIdInt int userId) {
         synchronized (mLock) {
             ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
             return evalForegroundOps(packageModes == null ? null : packageModes.get(packageName),
@@ -538,8 +537,8 @@
     }
 
     @Override
-    public boolean dumpListeners(int dumpOp, int dumpUid, @Nullable String dumpPackage,
-            @NonNull PrintWriter printWriter) {
+    public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage,
+            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 ef3e368..d8d0d48 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(@NonNull String packageName, @UserIdInt int userId);
+    boolean arePackageModesDefault(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, @Nullable SparseBooleanArray foregroundOps);
+    SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps);
 
     /**
      * Go over the list of app-ops for the package name and mark app-ops with MODE_FOREGROUND in
@@ -194,8 +194,8 @@
      * @param userId user id associated with the package.
      * @return foregroundOps.
      */
-    SparseBooleanArray evalForegroundPackageOps(@NonNull String packageName,
-            @Nullable SparseBooleanArray foregroundOps, @UserIdInt int userId);
+    SparseBooleanArray evalForegroundPackageOps(String packageName,
+            SparseBooleanArray foregroundOps, @UserIdInt int userId);
 
     /**
      * Dump op mode and package mode listeners and their details.
@@ -205,6 +205,5 @@
      * @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, @Nullable String dumpPackage,
-            @NonNull PrintWriter printWriter);
+    boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, PrintWriter printWriter);
 }
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java
deleted file mode 100644
index 4436002..0000000
--- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java
+++ /dev/null
@@ -1,279 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.appop;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
-import android.os.Trace;
-import android.util.ArraySet;
-import android.util.SparseBooleanArray;
-import android.util.SparseIntArray;
-
-import java.io.PrintWriter;
-
-/**
- * Surrounds all AppOpsCheckingServiceInterface method calls with Trace.traceBegin and
- * Trace.traceEnd. These traces are used for performance testing.
- */
-public class AppOpsCheckingServiceTracingDecorator implements AppOpsCheckingServiceInterface {
-    private static final long TRACE_TAG = Trace.TRACE_TAG_SYSTEM_SERVER;
-    private final AppOpsCheckingServiceInterface mService;
-
-    AppOpsCheckingServiceTracingDecorator(
-            @NonNull AppOpsCheckingServiceInterface appOpsCheckingServiceInterface) {
-        mService = appOpsCheckingServiceInterface;
-    }
-
-    @Override
-    public SparseIntArray getNonDefaultUidModes(int uid) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getNonDefaultUidModes");
-        try {
-            return mService.getNonDefaultUidModes(uid);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public int getUidMode(int uid, int op) {
-        Trace.traceBegin(TRACE_TAG, "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getUidMode");
-        try {
-            return mService.getUidMode(uid, op);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public boolean setUidMode(int uid, int op, @AppOpsManager.Mode int mode) {
-        Trace.traceBegin(TRACE_TAG, "TaggedTracingAppOpsCheckingServiceInterfaceImpl#setUidMode");
-        try {
-            return mService.setUidMode(uid, op, mode);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public int getPackageMode(@NonNull String packageName, int op, @UserIdInt int userId) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getPackageMode");
-        try {
-            return mService.getPackageMode(packageName, op, userId);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void setPackageMode(@NonNull String packageName, int op, @AppOpsManager.Mode int mode,
-            @UserIdInt int userId) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#setPackageMode");
-        try {
-            mService.setPackageMode(packageName, op, mode, userId);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public boolean removePackage(@NonNull String packageName, @UserIdInt int userId) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removePackage");
-        try {
-            return mService.removePackage(packageName, userId);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void removeUid(int uid) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removeUid");
-        try {
-            mService.removeUid(uid);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public boolean areUidModesDefault(int uid) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#areUidModesDefault");
-        try {
-            return mService.areUidModesDefault(uid);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public boolean arePackageModesDefault(String packageName, @UserIdInt int userId) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#arePackageModesDefault");
-        try {
-            return mService.arePackageModesDefault(packageName, userId);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void clearAllModes() {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#clearAllModes");
-        try {
-            mService.clearAllModes();
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void startWatchingOpModeChanged(@NonNull OnOpModeChangedListener changedListener,
-            int op) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#startWatchingOpModeChanged");
-        try {
-            mService.startWatchingOpModeChanged(changedListener, op);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void startWatchingPackageModeChanged(@NonNull OnOpModeChangedListener changedListener,
-            @NonNull String packageName) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#startWatchingPackageModeChanged");
-        try {
-            mService.startWatchingPackageModeChanged(changedListener, packageName);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void removeListener(@NonNull OnOpModeChangedListener changedListener) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removeListener");
-        try {
-            mService.removeListener(changedListener);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public ArraySet<OnOpModeChangedListener> getOpModeChangedListeners(int op) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getOpModeChangedListeners");
-        try {
-            return mService.getOpModeChangedListeners(op);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners(
-            @NonNull String packageName) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getPackageModeChangedListeners");
-        try {
-            return mService.getPackageModeChangedListeners(packageName);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void notifyWatchersOfChange(int op, int uid) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyWatchersOfChange");
-        try {
-            mService.notifyWatchersOfChange(op, uid);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void notifyOpChanged(@NonNull OnOpModeChangedListener changedListener, int op, int uid,
-            @Nullable String packageName) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyOpChanged");
-        try {
-            mService.notifyOpChanged(changedListener, op, uid, packageName);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public void notifyOpChangedForAllPkgsInUid(int op, int uid, boolean onlyForeground,
-            @Nullable OnOpModeChangedListener callbackToIgnore) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyOpChangedForAllPkgsInUid");
-        try {
-            mService.notifyOpChangedForAllPkgsInUid(op, uid, onlyForeground, callbackToIgnore);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#evalForegroundUidOps");
-        try {
-            return mService.evalForegroundUidOps(uid, foregroundOps);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public SparseBooleanArray evalForegroundPackageOps(String packageName,
-            SparseBooleanArray foregroundOps, @UserIdInt int userId) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#evalForegroundPackageOps");
-        try {
-            return mService.evalForegroundPackageOps(packageName, foregroundOps, userId);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-
-    @Override
-    public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage,
-            PrintWriter printWriter) {
-        Trace.traceBegin(TRACE_TAG,
-                "TaggedTracingAppOpsCheckingServiceInterfaceImpl#dumpListeners");
-        try {
-            return mService.dumpListeners(dumpOp, dumpUid, dumpPackage, printWriter);
-        } finally {
-            Trace.traceEnd(TRACE_TAG);
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java b/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java
index af5b07e..f51200f2 100644
--- a/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java
@@ -42,7 +42,7 @@
 
     private Context mContext;
     private Handler mHandler;
-    private AppOpsCheckingServiceInterface mAppOpsServiceInterface;
+    private AppOpsCheckingServiceInterface mAppOpsCheckingServiceInterface;
 
     // Map from (Object token) to (int code) to (boolean restricted)
     private final ArrayMap<Object, SparseBooleanArray> mGlobalRestrictions = new ArrayMap<>();
@@ -56,10 +56,10 @@
             mUserRestrictionExcludedPackageTags = new ArrayMap<>();
 
     public AppOpsRestrictionsImpl(Context context, Handler handler,
-            AppOpsCheckingServiceInterface appOpsServiceInterface) {
+            AppOpsCheckingServiceInterface appOpsCheckingServiceInterface) {
         mContext = context;
         mHandler = handler;
-        mAppOpsServiceInterface = appOpsServiceInterface;
+        mAppOpsCheckingServiceInterface = appOpsCheckingServiceInterface;
     }
 
     @Override
@@ -219,7 +219,7 @@
         int restrictedCodesSize = allUserRestrictedCodes.size();
         for (int j = 0; j < restrictedCodesSize; j++) {
             int code = allUserRestrictedCodes.keyAt(j);
-            mHandler.post(() -> mAppOpsServiceInterface.notifyWatchersOfChange(code, UID_ANY));
+            mHandler.post(() -> mAppOpsCheckingServiceInterface.notifyWatchersOfChange(code, UID_ANY));
         }
     }
 
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 39338c6..9345422 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -18,22 +18,56 @@
 
 import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
 import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED;
+import static android.app.AppOpsManager.CALL_BACK_ON_SWITCHED_OP;
+import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG;
+import static android.app.AppOpsManager.FILTER_BY_OP_NAMES;
+import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME;
+import static android.app.AppOpsManager.FILTER_BY_UID;
+import static android.app.AppOpsManager.HISTORY_FLAG_GET_ATTRIBUTION_CHAINS;
+import static android.app.AppOpsManager.HistoricalOpsRequestFilter;
+import static android.app.AppOpsManager.KEY_BG_STATE_SETTLE_TIME;
+import static android.app.AppOpsManager.KEY_FG_SERVICE_STATE_SETTLE_TIME;
+import static android.app.AppOpsManager.KEY_TOP_STATE_SETTLE_TIME;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.app.AppOpsManager.MODE_ERRORED;
+import static android.app.AppOpsManager.MODE_FOREGROUND;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OP_CAMERA;
 import static android.app.AppOpsManager.OP_FLAGS_ALL;
 import static android.app.AppOpsManager.OP_FLAG_SELF;
 import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
 import static android.app.AppOpsManager.OP_NONE;
+import static android.app.AppOpsManager.OP_PLAY_AUDIO;
+import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
+import static android.app.AppOpsManager.OP_RECORD_AUDIO;
+import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
+import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM;
+import static android.app.AppOpsManager.OP_VIBRATE;
+import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED;
+import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED;
+import static android.app.AppOpsManager.OpEventProxyInfo;
+import static android.app.AppOpsManager.RestrictionBypass;
 import static android.app.AppOpsManager.SAMPLING_STRATEGY_BOOT_TIME_SAMPLING;
 import static android.app.AppOpsManager.SAMPLING_STRATEGY_RARELY_USED;
 import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM;
 import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM_OPS;
+import static android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE;
 import static android.app.AppOpsManager._NUM_OP;
+import static android.app.AppOpsManager.extractFlagsFromKey;
+import static android.app.AppOpsManager.extractUidStateFromKey;
+import static android.app.AppOpsManager.modeToName;
+import static android.app.AppOpsManager.opAllowSystemBypassRestriction;
 import static android.app.AppOpsManager.opRestrictsRead;
+import static android.app.AppOpsManager.opToName;
 import static android.app.AppOpsManager.opToPublicName;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
+import static android.content.Intent.EXTRA_REPLACING;
 import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
 import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
 
+import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -42,16 +76,21 @@
 import android.app.ActivityManagerInternal;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
+import android.app.AppOpsManager.AttributedOpEntry;
 import android.app.AppOpsManager.AttributionFlags;
 import android.app.AppOpsManager.HistoricalOps;
+import android.app.AppOpsManager.Mode;
+import android.app.AppOpsManager.OpEntry;
 import android.app.AppOpsManager.OpFlags;
 import android.app.AppOpsManagerInternal;
 import android.app.AppOpsManagerInternal.CheckOpsDelegate;
 import android.app.AsyncNotedAppOp;
 import android.app.RuntimeAppOpAccessMessage;
 import android.app.SyncNotedAppOp;
+import android.app.admin.DevicePolicyManagerInternal;
 import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -59,11 +98,15 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.PermissionInfo;
+import android.database.ContentObserver;
 import android.hardware.camera2.CameraDevice.CAMERA_AUDIO_RESTRICTION;
+import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.PackageTagsList;
 import android.os.Process;
@@ -74,14 +117,22 @@
 import android.os.ServiceManager;
 import android.os.ShellCallback;
 import android.os.ShellCommand;
+import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.storage.StorageManagerInternal;
+import android.permission.PermissionManager;
+import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.KeyValueListParser;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.TimeUtils;
+import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.Immutable;
@@ -93,37 +144,61 @@
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.app.IAppOpsStartedCallback;
 import com.android.internal.app.MessageSamplingConfig;
+import com.android.internal.compat.IPlatformCompat;
+import com.android.internal.os.Clock;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
+import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
+import com.android.server.LockGuard;
+import com.android.server.SystemServerInitThreadPool;
 import com.android.server.SystemServiceManager;
 import com.android.server.pm.PackageList;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.component.ParsedAttribution;
 import com.android.server.policy.AppOpsPolicy;
 
+import dalvik.annotation.optimization.NeverCompile;
+
+import libcore.util.EmptyArray;
+
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Scanner;
+import java.util.Set;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.Consumer;
 
-/**
- * The system service component to {@link AppOpsManager}.
- */
-public class AppOpsService extends IAppOpsService.Stub {
-
-    private final AppOpsServiceInterface mAppOpsService;
-
+public class AppOpsService extends IAppOpsService.Stub implements PersistenceScheduler {
     static final String TAG = "AppOps";
     static final boolean DEBUG = false;
 
@@ -132,19 +207,74 @@
      */
     private final ArraySet<NoteOpTrace> mNoteOpCallerStacktraces = new ArraySet<>();
 
+    /**
+     * Sentinel integer version to denote that there was no appops.xml found on boot.
+     * This will happen when a device boots with no existing userdata.
+     */
+    private static final int NO_FILE_VERSION = -2;
+
+    /**
+     * Sentinel integer version to denote that there was no version in the appops.xml found on boot.
+     * This means the file is coming from a build before versioning was added.
+     */
+    private static final int NO_VERSION = -1;
+
+    /** Increment by one every time and add the corresponding upgrade logic in
+     *  {@link #upgradeLocked(int)} below. The first version was 1 */
+    static final int CURRENT_VERSION = 2;
+
+    /**
+     * This stores the version of appops.xml seen at boot. If this is smaller than
+     * {@link #CURRENT_VERSION}, then we will run {@link #upgradeLocked(int)} on startup.
+     */
+    private int mVersionAtBoot = NO_FILE_VERSION;
+
+    // Write at most every 30 minutes.
+    static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000;
+
     // Constant meaning that any UID should be matched when dispatching callbacks
     private static final int UID_ANY = -2;
 
-    private static final int MAX_UNFORWARDED_OPS = 10;
+    private static final int[] OPS_RESTRICTED_ON_SUSPEND = {
+            OP_PLAY_AUDIO,
+            OP_RECORD_AUDIO,
+            OP_CAMERA,
+            OP_VIBRATE,
+    };
 
+    private static final int MAX_UNFORWARDED_OPS = 10;
+    private static final int MAX_UNUSED_POOLED_OBJECTS = 3;
     private static final int RARELY_USED_PACKAGES_INITIALIZATION_DELAY_MILLIS = 300000;
 
     final Context mContext;
+    final AtomicFile mFile;
     private final @Nullable File mNoteOpCallerStacktracesFile;
     final Handler mHandler;
 
+    /**
+     * Pool for {@link AttributedOp.OpEventProxyInfoPool} to avoid to constantly reallocate new
+     * objects
+     */
+    @GuardedBy("this")
+    final AttributedOp.OpEventProxyInfoPool mOpEventProxyInfoPool =
+            new AttributedOp.OpEventProxyInfoPool(MAX_UNUSED_POOLED_OBJECTS);
+
+    /**
+     * Pool for {@link AttributedOp.InProgressStartOpEventPool} to avoid to constantly reallocate
+     * new objects
+     */
+    @GuardedBy("this")
+    final AttributedOp.InProgressStartOpEventPool mInProgressStartOpEventPool =
+            new AttributedOp.InProgressStartOpEventPool(mOpEventProxyInfoPool,
+                    MAX_UNUSED_POOLED_OBJECTS);
+
     private final AppOpsManagerInternalImpl mAppOpsManagerInternal
             = new AppOpsManagerInternalImpl();
+    @Nullable private final DevicePolicyManagerInternal dpmi =
+            LocalServices.getService(DevicePolicyManagerInternal.class);
+
+    private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface(
+            ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
 
     /**
      * Registered callbacks, called from {@link #collectAsyncNotedOp}.
@@ -171,9 +301,54 @@
 
     boolean mWriteNoteOpsScheduled;
 
+    boolean mWriteScheduled;
+    boolean mFastWriteScheduled;
+    final Runnable mWriteRunner = new Runnable() {
+        public void run() {
+            synchronized (AppOpsService.this) {
+                mWriteScheduled = false;
+                mFastWriteScheduled = false;
+                AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
+                    @Override protected Void doInBackground(Void... params) {
+                        writeState();
+                        return null;
+                    }
+                };
+                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
+            }
+        }
+    };
+
+    @GuardedBy("this")
+    @VisibleForTesting
+    final SparseArray<UidState> mUidStates = new SparseArray<>();
+
+    volatile @NonNull HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this);
+
+    /*
+     * These are app op restrictions imposed per user from various parties.
+     */
+    private final ArrayMap<IBinder, ClientUserRestrictionState> mOpUserRestrictions =
+            new ArrayMap<>();
+
+    /*
+     * These are app op restrictions imposed globally from various parties within the system.
+     */
+    private final ArrayMap<IBinder, ClientGlobalRestrictionState> mOpGlobalRestrictions =
+            new ArrayMap<>();
+
+    SparseIntArray mProfileOwners;
+
     private volatile CheckOpsDelegateDispatcher mCheckOpsDelegateDispatcher =
             new CheckOpsDelegateDispatcher(/*policy*/ null, /*delegate*/ null);
 
+    /**
+      * Reverse lookup for {@link AppOpsManager#opToSwitch(int)}. Initialized once and never
+      * changed
+      */
+    private final SparseArray<int[]> mSwitchedOps = new SparseArray<>();
+
+    private ActivityManagerInternal mActivityManagerInternal;
 
     /** Package sampled for message collection in the current session */
     @GuardedBy("this")
@@ -207,8 +382,546 @@
     /** Package Manager internal. Access via {@link #getPackageManagerInternal()} */
     private @Nullable PackageManagerInternal mPackageManagerInternal;
 
+    /** Interface for app-op modes.*/
+    @VisibleForTesting
+    AppOpsCheckingServiceInterface mAppOpsCheckingService;
+
+    /** Interface for app-op restrictions.*/
+    @VisibleForTesting AppOpsRestrictions mAppOpsRestrictions;
+
+    private AppOpsUidStateTracker mUidStateTracker;
+
+    /** Hands the definition of foreground and uid states */
+    @GuardedBy("this")
+    public AppOpsUidStateTracker getUidStateTracker() {
+        if (mUidStateTracker == null) {
+            mUidStateTracker = new AppOpsUidStateTrackerImpl(
+                    LocalServices.getService(ActivityManagerInternal.class),
+                    mHandler,
+                    r -> {
+                        synchronized (AppOpsService.this) {
+                            r.run();
+                        }
+                    },
+                    Clock.SYSTEM_CLOCK, mConstants);
+
+            mUidStateTracker.addUidStateChangedCallback(new HandlerExecutor(mHandler),
+                    this::onUidStateChanged);
+        }
+        return mUidStateTracker;
+    }
+
+    /**
+     * All times are in milliseconds. These constants are kept synchronized with the system
+     * global Settings. Any access to this class or its fields should be done while
+     * holding the AppOpsService lock.
+     */
+    final class Constants extends ContentObserver {
+
+        /**
+         * How long we want for a drop in uid state from top to settle before applying it.
+         * @see Settings.Global#APP_OPS_CONSTANTS
+         * @see AppOpsManager#KEY_TOP_STATE_SETTLE_TIME
+         */
+        public long TOP_STATE_SETTLE_TIME;
+
+        /**
+         * How long we want for a drop in uid state from foreground to settle before applying it.
+         * @see Settings.Global#APP_OPS_CONSTANTS
+         * @see AppOpsManager#KEY_FG_SERVICE_STATE_SETTLE_TIME
+         */
+        public long FG_SERVICE_STATE_SETTLE_TIME;
+
+        /**
+         * How long we want for a drop in uid state from background to settle before applying it.
+         * @see Settings.Global#APP_OPS_CONSTANTS
+         * @see AppOpsManager#KEY_BG_STATE_SETTLE_TIME
+         */
+        public long BG_STATE_SETTLE_TIME;
+
+        private final KeyValueListParser mParser = new KeyValueListParser(',');
+        private ContentResolver mResolver;
+
+        public Constants(Handler handler) {
+            super(handler);
+            updateConstants();
+        }
+
+        public void startMonitoring(ContentResolver resolver) {
+            mResolver = resolver;
+            mResolver.registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.APP_OPS_CONSTANTS),
+                    false, this);
+            updateConstants();
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            updateConstants();
+        }
+
+        private void updateConstants() {
+            String value = mResolver != null ? Settings.Global.getString(mResolver,
+                    Settings.Global.APP_OPS_CONSTANTS) : "";
+
+            synchronized (AppOpsService.this) {
+                try {
+                    mParser.setString(value);
+                } catch (IllegalArgumentException e) {
+                    // Failed to parse the settings string, log this and move on
+                    // with defaults.
+                    Slog.e(TAG, "Bad app ops settings", e);
+                }
+                TOP_STATE_SETTLE_TIME = mParser.getDurationMillis(
+                        KEY_TOP_STATE_SETTLE_TIME, 5 * 1000L);
+                FG_SERVICE_STATE_SETTLE_TIME = mParser.getDurationMillis(
+                        KEY_FG_SERVICE_STATE_SETTLE_TIME, 5 * 1000L);
+                BG_STATE_SETTLE_TIME = mParser.getDurationMillis(
+                        KEY_BG_STATE_SETTLE_TIME, 1 * 1000L);
+            }
+        }
+
+        void dump(PrintWriter pw) {
+            pw.println("  Settings:");
+
+            pw.print("    "); pw.print(KEY_TOP_STATE_SETTLE_TIME); pw.print("=");
+            TimeUtils.formatDuration(TOP_STATE_SETTLE_TIME, pw);
+            pw.println();
+            pw.print("    "); pw.print(KEY_FG_SERVICE_STATE_SETTLE_TIME); pw.print("=");
+            TimeUtils.formatDuration(FG_SERVICE_STATE_SETTLE_TIME, pw);
+            pw.println();
+            pw.print("    "); pw.print(KEY_BG_STATE_SETTLE_TIME); pw.print("=");
+            TimeUtils.formatDuration(BG_STATE_SETTLE_TIME, pw);
+            pw.println();
+        }
+    }
+
+    @VisibleForTesting
+    final Constants mConstants;
+
+    @VisibleForTesting
+    final class UidState {
+        public final int uid;
+
+        public ArrayMap<String, Ops> pkgOps;
+
+        // true indicates there is an interested observer, false there isn't but it has such an op
+        //TODO: Move foregroundOps and hasForegroundWatchers into the AppOpsServiceInterface.
+        public SparseBooleanArray foregroundOps;
+        public boolean hasForegroundWatchers;
+
+        public UidState(int uid) {
+            this.uid = uid;
+        }
+
+        public void clear() {
+            mAppOpsCheckingService.removeUid(uid);
+            if (pkgOps != null) {
+                for (String packageName : pkgOps.keySet()) {
+                    mAppOpsCheckingService.removePackage(packageName, UserHandle.getUserId(uid));
+                }
+            }
+            pkgOps = null;
+        }
+
+        public boolean isDefault() {
+            boolean areAllPackageModesDefault = true;
+            if (pkgOps != null) {
+                for (String packageName : pkgOps.keySet()) {
+                    if (!mAppOpsCheckingService.arePackageModesDefault(packageName,
+                            UserHandle.getUserId(uid))) {
+                        areAllPackageModesDefault = false;
+                        break;
+                    }
+                }
+            }
+            return (pkgOps == null || pkgOps.isEmpty())
+                    && mAppOpsCheckingService.areUidModesDefault(uid)
+                    && areAllPackageModesDefault;
+        }
+
+        // Functions for uid mode access and manipulation.
+        public SparseIntArray getNonDefaultUidModes() {
+            return mAppOpsCheckingService.getNonDefaultUidModes(uid);
+        }
+
+        public int getUidMode(int op) {
+            return mAppOpsCheckingService.getUidMode(uid, op);
+        }
+
+        public boolean setUidMode(int op, int mode) {
+            return mAppOpsCheckingService.setUidMode(uid, op, mode);
+        }
+
+        @SuppressWarnings("GuardedBy")
+        int evalMode(int op, int mode) {
+            return getUidStateTracker().evalMode(uid, op, mode);
+        }
+
+        public void evalForegroundOps() {
+            foregroundOps = null;
+            foregroundOps = mAppOpsCheckingService.evalForegroundUidOps(uid, foregroundOps);
+            if (pkgOps != null) {
+                for (int i = pkgOps.size() - 1; i >= 0; i--) {
+                    foregroundOps = mAppOpsCheckingService
+                            .evalForegroundPackageOps(pkgOps.valueAt(i).packageName, foregroundOps,
+                                    UserHandle.getUserId(uid));
+                }
+            }
+            hasForegroundWatchers = false;
+            if (foregroundOps != null) {
+                for (int i = 0;  i < foregroundOps.size(); i++) {
+                    if (foregroundOps.valueAt(i)) {
+                        hasForegroundWatchers = true;
+                        break;
+                    }
+                }
+            }
+        }
+
+        @SuppressWarnings("GuardedBy")
+        public int getState() {
+            return getUidStateTracker().getUidState(uid);
+        }
+
+        @SuppressWarnings("GuardedBy")
+        public void dump(PrintWriter pw, long nowElapsed) {
+            getUidStateTracker().dumpUidState(pw, uid, nowElapsed);
+        }
+    }
+
+    final static class Ops extends SparseArray<Op> {
+        final String packageName;
+        final UidState uidState;
+
+        /**
+         * The restriction properties of the package. If {@code null} it could not have been read
+         * yet and has to be refreshed.
+         */
+        @Nullable RestrictionBypass bypass;
+
+        /** Lazily populated cache of attributionTags of this package */
+        final @NonNull ArraySet<String> knownAttributionTags = new ArraySet<>();
+
+        /**
+         * Lazily populated cache of <b>valid</b> attributionTags of this package, a set smaller
+         * than or equal to {@link #knownAttributionTags}.
+         */
+        final @NonNull ArraySet<String> validAttributionTags = new ArraySet<>();
+
+        Ops(String _packageName, UidState _uidState) {
+            packageName = _packageName;
+            uidState = _uidState;
+        }
+    }
+
+    /** Returned from {@link #verifyAndGetBypass(int, String, String, String)}. */
+    private static final class PackageVerificationResult {
+
+        final RestrictionBypass bypass;
+        final boolean isAttributionTagValid;
+
+        PackageVerificationResult(RestrictionBypass bypass, boolean isAttributionTagValid) {
+            this.bypass = bypass;
+            this.isAttributionTagValid = isAttributionTagValid;
+        }
+    }
+
+    final class Op {
+        int op;
+        int uid;
+        final UidState uidState;
+        final @NonNull String packageName;
+
+        /** attributionTag -> AttributedOp */
+        final ArrayMap<String, AttributedOp> mAttributions = new ArrayMap<>(1);
+
+        Op(UidState uidState, String packageName, int op, int uid) {
+            this.op = op;
+            this.uid = uid;
+            this.uidState = uidState;
+            this.packageName = packageName;
+        }
+
+        @Mode int getMode() {
+            return mAppOpsCheckingService.getPackageMode(packageName, this.op,
+                    UserHandle.getUserId(this.uid));
+        }
+        void setMode(@Mode int mode) {
+            mAppOpsCheckingService.setPackageMode(packageName, this.op, mode,
+                    UserHandle.getUserId(this.uid));
+        }
+
+        void removeAttributionsWithNoTime() {
+            for (int i = mAttributions.size() - 1; i >= 0; i--) {
+                if (!mAttributions.valueAt(i).hasAnyTime()) {
+                    mAttributions.removeAt(i);
+                }
+            }
+        }
+
+        private @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent,
+                @Nullable String attributionTag) {
+            AttributedOp attributedOp;
+
+            attributedOp = mAttributions.get(attributionTag);
+            if (attributedOp == null) {
+                attributedOp = new AttributedOp(AppOpsService.this, attributionTag, parent);
+                mAttributions.put(attributionTag, attributedOp);
+            }
+
+            return attributedOp;
+        }
+
+        @NonNull OpEntry createEntryLocked() {
+            final int numAttributions = mAttributions.size();
+
+            final ArrayMap<String, AppOpsManager.AttributedOpEntry> attributionEntries =
+                    new ArrayMap<>(numAttributions);
+            for (int i = 0; i < numAttributions; i++) {
+                attributionEntries.put(mAttributions.keyAt(i),
+                        mAttributions.valueAt(i).createAttributedOpEntryLocked());
+            }
+
+            return new OpEntry(op, getMode(), attributionEntries);
+        }
+
+        @NonNull OpEntry createSingleAttributionEntryLocked(@Nullable String attributionTag) {
+            final int numAttributions = mAttributions.size();
+
+            final ArrayMap<String, AttributedOpEntry> attributionEntries = new ArrayMap<>(1);
+            for (int i = 0; i < numAttributions; i++) {
+                if (Objects.equals(mAttributions.keyAt(i), attributionTag)) {
+                    attributionEntries.put(mAttributions.keyAt(i),
+                            mAttributions.valueAt(i).createAttributedOpEntryLocked());
+                    break;
+                }
+            }
+
+            return new OpEntry(op, getMode(), attributionEntries);
+        }
+
+        boolean isRunning() {
+            final int numAttributions = mAttributions.size();
+            for (int i = 0; i < numAttributions; i++) {
+                if (mAttributions.valueAt(i).isRunning()) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+    }
+
+    final ArrayMap<IBinder, ModeCallback> mModeWatchers = new ArrayMap<>();
+    final ArrayMap<IBinder, SparseArray<ActiveCallback>> mActiveWatchers = new ArrayMap<>();
+    final ArrayMap<IBinder, SparseArray<StartedCallback>> mStartedWatchers = new ArrayMap<>();
+    final ArrayMap<IBinder, SparseArray<NotedCallback>> mNotedWatchers = new ArrayMap<>();
     final AudioRestrictionManager mAudioRestrictionManager = new AudioRestrictionManager();
 
+    final class ModeCallback extends OnOpModeChangedListener implements DeathRecipient  {
+        /** If mWatchedOpCode==ALL_OPS notify for ops affected by the switch-op */
+        public static final int ALL_OPS = -2;
+
+        // Need to keep this only because stopWatchingMode needs an IAppOpsCallback.
+        // Otherwise we can just use the IBinder object.
+        private final IAppOpsCallback mCallback;
+
+        ModeCallback(IAppOpsCallback callback, int watchingUid, int flags, int watchedOpCode,
+                int callingUid, int callingPid) {
+            super(watchingUid, flags, watchedOpCode, callingUid, callingPid);
+            this.mCallback = callback;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("ModeCallback{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" watchinguid=");
+            UserHandle.formatUid(sb, getWatchingUid());
+            sb.append(" flags=0x");
+            sb.append(Integer.toHexString(getFlags()));
+            switch (getWatchedOpCode()) {
+                case OP_NONE:
+                    break;
+                case ALL_OPS:
+                    sb.append(" op=(all)");
+                    break;
+                default:
+                    sb.append(" op=");
+                    sb.append(opToName(getWatchedOpCode()));
+                    break;
+            }
+            sb.append(" from uid=");
+            UserHandle.formatUid(sb, getCallingUid());
+            sb.append(" pid=");
+            sb.append(getCallingPid());
+            sb.append('}');
+            return sb.toString();
+        }
+
+        void unlinkToDeath() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void binderDied() {
+            stopWatchingMode(mCallback);
+        }
+
+        @Override
+        public void onOpModeChanged(int op, int uid, String packageName) throws RemoteException {
+            mCallback.opChanged(op, uid, packageName);
+        }
+    }
+
+    final class ActiveCallback implements DeathRecipient {
+        final IAppOpsActiveCallback mCallback;
+        final int mWatchingUid;
+        final int mCallingUid;
+        final int mCallingPid;
+
+        ActiveCallback(IAppOpsActiveCallback callback, int watchingUid, int callingUid,
+                int callingPid) {
+            mCallback = callback;
+            mWatchingUid = watchingUid;
+            mCallingUid = callingUid;
+            mCallingPid = callingPid;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("ActiveCallback{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" watchinguid=");
+            UserHandle.formatUid(sb, mWatchingUid);
+            sb.append(" from uid=");
+            UserHandle.formatUid(sb, mCallingUid);
+            sb.append(" pid=");
+            sb.append(mCallingPid);
+            sb.append('}');
+            return sb.toString();
+        }
+
+        void destroy() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void binderDied() {
+            stopWatchingActive(mCallback);
+        }
+    }
+
+    final class StartedCallback implements DeathRecipient {
+        final IAppOpsStartedCallback mCallback;
+        final int mWatchingUid;
+        final int mCallingUid;
+        final int mCallingPid;
+
+        StartedCallback(IAppOpsStartedCallback callback, int watchingUid, int callingUid,
+                int callingPid) {
+            mCallback = callback;
+            mWatchingUid = watchingUid;
+            mCallingUid = callingUid;
+            mCallingPid = callingPid;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("StartedCallback{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" watchinguid=");
+            UserHandle.formatUid(sb, mWatchingUid);
+            sb.append(" from uid=");
+            UserHandle.formatUid(sb, mCallingUid);
+            sb.append(" pid=");
+            sb.append(mCallingPid);
+            sb.append('}');
+            return sb.toString();
+        }
+
+        void destroy() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void binderDied() {
+            stopWatchingStarted(mCallback);
+        }
+    }
+
+    final class NotedCallback implements DeathRecipient {
+        final IAppOpsNotedCallback mCallback;
+        final int mWatchingUid;
+        final int mCallingUid;
+        final int mCallingPid;
+
+        NotedCallback(IAppOpsNotedCallback callback, int watchingUid, int callingUid,
+                int callingPid) {
+            mCallback = callback;
+            mWatchingUid = watchingUid;
+            mCallingUid = callingUid;
+            mCallingPid = callingPid;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("NotedCallback{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" watchinguid=");
+            UserHandle.formatUid(sb, mWatchingUid);
+            sb.append(" from uid=");
+            UserHandle.formatUid(sb, mCallingUid);
+            sb.append(" pid=");
+            sb.append(mCallingPid);
+            sb.append('}');
+            return sb.toString();
+        }
+
+        void destroy() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void binderDied() {
+            stopWatchingNoted(mCallback);
+        }
+    }
+
+    /**
+     * Call {@link AttributedOp#onClientDeath attributedOp.onClientDeath(clientId)}.
+     */
+    static void onClientDeath(@NonNull AttributedOp attributedOp,
+            @NonNull IBinder clientId) {
+        attributedOp.onClientDeath(clientId);
+    }
+
+
     /**
      * Loads the OpsValidation file results into a hashmap {@link #mNoteOpCallerStacktraces}
      * so that we do not log the same operation twice between instances
@@ -233,12 +946,20 @@
     }
 
     public AppOpsService(File storagePath, Handler handler, Context context) {
-        this(handler, context, new AppOpsServiceImpl(storagePath, handler, context));
-    }
+        mContext = context;
 
-    @VisibleForTesting
-    public AppOpsService(Handler handler, Context context,
-            AppOpsServiceInterface appOpsServiceInterface) {
+        for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) {
+            int switchCode = AppOpsManager.opToSwitch(switchedCode);
+            mSwitchedOps.put(switchCode,
+                    ArrayUtils.appendInt(mSwitchedOps.get(switchCode), switchedCode));
+        }
+        mAppOpsCheckingService =
+                new AppOpsCheckingServiceImpl(this, this, handler, context, mSwitchedOps);
+        mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler,
+                mAppOpsCheckingService);
+
+        LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
+        mFile = new AtomicFile(storagePath, "appops");
         if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED) {
             mNoteOpCallerStacktracesFile = new File(SystemServiceManager.ensureSystemDir(),
                     "noteOpStackTraces.json");
@@ -246,25 +967,189 @@
         } else {
             mNoteOpCallerStacktracesFile = null;
         }
-
-        mAppOpsService = appOpsServiceInterface;
-        mContext = context;
         mHandler = handler;
+        mConstants = new Constants(mHandler);
+        readState();
     }
 
-    /**
-     * Publishes binder and local service.
-     */
     public void publish() {
         ServiceManager.addService(Context.APP_OPS_SERVICE, asBinder());
         LocalServices.addService(AppOpsManagerInternal.class, mAppOpsManagerInternal);
     }
 
-    /**
-     * Finishes boot sequence.
-     */
+    /** Handler for work when packages are removed or updated */
+    private BroadcastReceiver mOnPackageUpdatedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            String pkgName = intent.getData().getEncodedSchemeSpecificPart();
+            int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
+
+            if (action.equals(ACTION_PACKAGE_REMOVED) && !intent.hasExtra(EXTRA_REPLACING)) {
+                synchronized (AppOpsService.this) {
+                    UidState uidState = mUidStates.get(uid);
+                    if (uidState == null || uidState.pkgOps == null) {
+                        return;
+                    }
+                    mAppOpsCheckingService.removePackage(pkgName, UserHandle.getUserId(uid));
+                    Ops removedOps = uidState.pkgOps.remove(pkgName);
+                    if (removedOps != null) {
+                        scheduleFastWriteLocked();
+                    }
+                }
+            } else if (action.equals(Intent.ACTION_PACKAGE_REPLACED)) {
+                AndroidPackage pkg = getPackageManagerInternal().getPackage(pkgName);
+                if (pkg == null) {
+                    return;
+                }
+
+                ArrayMap<String, String> dstAttributionTags = new ArrayMap<>();
+                ArraySet<String> attributionTags = new ArraySet<>();
+                attributionTags.add(null);
+                if (pkg.getAttributions() != null) {
+                    int numAttributions = pkg.getAttributions().size();
+                    for (int attributionNum = 0; attributionNum < numAttributions;
+                            attributionNum++) {
+                        ParsedAttribution attribution = pkg.getAttributions().get(attributionNum);
+                        attributionTags.add(attribution.getTag());
+
+                        int numInheritFrom = attribution.getInheritFrom().size();
+                        for (int inheritFromNum = 0; inheritFromNum < numInheritFrom;
+                                inheritFromNum++) {
+                            dstAttributionTags.put(attribution.getInheritFrom().get(inheritFromNum),
+                                    attribution.getTag());
+                        }
+                    }
+                }
+
+                synchronized (AppOpsService.this) {
+                    UidState uidState = mUidStates.get(uid);
+                    if (uidState == null || uidState.pkgOps == null) {
+                        return;
+                    }
+
+                    Ops ops = uidState.pkgOps.get(pkgName);
+                    if (ops == null) {
+                        return;
+                    }
+
+                    // Reset cached package properties to re-initialize when needed
+                    ops.bypass = null;
+                    ops.knownAttributionTags.clear();
+
+                    // Merge data collected for removed attributions into their successor
+                    // attributions
+                    int numOps = ops.size();
+                    for (int opNum = 0; opNum < numOps; opNum++) {
+                        Op op = ops.valueAt(opNum);
+
+                        int numAttributions = op.mAttributions.size();
+                        for (int attributionNum = numAttributions - 1; attributionNum >= 0;
+                                attributionNum--) {
+                            String attributionTag = op.mAttributions.keyAt(attributionNum);
+
+                            if (attributionTags.contains(attributionTag)) {
+                                // attribution still exist after upgrade
+                                continue;
+                            }
+
+                            String newAttributionTag = dstAttributionTags.get(attributionTag);
+
+                            AttributedOp newAttributedOp = op.getOrCreateAttribution(op,
+                                    newAttributionTag);
+                            newAttributedOp.add(op.mAttributions.valueAt(attributionNum));
+                            op.mAttributions.removeAt(attributionNum);
+
+                            scheduleFastWriteLocked();
+                        }
+                    }
+                }
+            }
+        }
+    };
+
     public void systemReady() {
-        mAppOpsService.systemReady();
+        synchronized (this) {
+            upgradeLocked(mVersionAtBoot);
+        }
+
+        mConstants.startMonitoring(mContext.getContentResolver());
+        mHistoricalRegistry.systemReady(mContext.getContentResolver());
+
+        IntentFilter packageUpdateFilter = new IntentFilter();
+        packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+        packageUpdateFilter.addDataScheme("package");
+
+        mContext.registerReceiverAsUser(mOnPackageUpdatedReceiver, UserHandle.ALL,
+                packageUpdateFilter, null, null);
+
+        synchronized (this) {
+            for (int uidNum = mUidStates.size() - 1; uidNum >= 0; uidNum--) {
+                int uid = mUidStates.keyAt(uidNum);
+                UidState uidState = mUidStates.valueAt(uidNum);
+
+                String[] pkgsInUid = getPackagesForUid(uidState.uid);
+                if (ArrayUtils.isEmpty(pkgsInUid)) {
+                    uidState.clear();
+                    mUidStates.removeAt(uidNum);
+                    scheduleFastWriteLocked();
+                    continue;
+                }
+
+                ArrayMap<String, Ops> pkgs = uidState.pkgOps;
+                if (pkgs == null) {
+                    continue;
+                }
+
+                int numPkgs = pkgs.size();
+                for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
+                    String pkg = pkgs.keyAt(pkgNum);
+
+                    String action;
+                    if (!ArrayUtils.contains(pkgsInUid, pkg)) {
+                        action = Intent.ACTION_PACKAGE_REMOVED;
+                    } else {
+                        action = Intent.ACTION_PACKAGE_REPLACED;
+                    }
+
+                    SystemServerInitThreadPool.submit(
+                            () -> mOnPackageUpdatedReceiver.onReceive(mContext, new Intent(action)
+                                    .setData(Uri.fromParts("package", pkg, null))
+                                    .putExtra(Intent.EXTRA_UID, uid)),
+                            "Update app-ops uidState in case package " + pkg + " changed");
+                }
+            }
+        }
+
+        final IntentFilter packageSuspendFilter = new IntentFilter();
+        packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
+        packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+        mContext.registerReceiverAsUser(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final int[] changedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST);
+                final String[] changedPkgs = intent.getStringArrayExtra(
+                        Intent.EXTRA_CHANGED_PACKAGE_LIST);
+                for (int code : OPS_RESTRICTED_ON_SUSPEND) {
+                    ArraySet<OnOpModeChangedListener> onModeChangedListeners;
+                    synchronized (AppOpsService.this) {
+                        onModeChangedListeners =
+                                mAppOpsCheckingService.getOpModeChangedListeners(code);
+                        if (onModeChangedListeners == null) {
+                            continue;
+                        }
+                    }
+                    for (int i = 0; i < changedUids.length; i++) {
+                        final int changedUid = changedUids[i];
+                        final String changedPkg = changedPkgs[i];
+                        // We trust packagemanager to insert matching uid and packageNames in the
+                        // extras
+                        notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg);
+                    }
+                }
+            }
+        }, UserHandle.ALL, packageSuspendFilter, null, null);
 
         final IntentFilter packageAddedFilter = new IntentFilter();
         packageAddedFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
@@ -272,8 +1157,9 @@
         mContext.registerReceiver(new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
+                final Uri data = intent.getData();
 
-                final String packageName = intent.getData().getSchemeSpecificPart();
+                final String packageName = data.getSchemeSpecificPart();
                 PackageInfo pi = getPackageManagerInternal().getPackageInfo(packageName,
                         PackageManager.GET_PERMISSIONS, Process.myUid(), mContext.getUserId());
                 if (isSamplingTarget(pi)) {
@@ -308,6 +1194,8 @@
                         }
                     }
                 });
+
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
     }
 
     /**
@@ -322,18 +1210,132 @@
         mCheckOpsDelegateDispatcher = new CheckOpsDelegateDispatcher(policy, delegate);
     }
 
-    /**
-     * Notify when a package is removed
-     */
     public void packageRemoved(int uid, String packageName) {
-        mAppOpsService.packageRemoved(uid, packageName);
+        synchronized (this) {
+            UidState uidState = mUidStates.get(uid);
+            if (uidState == null) {
+                return;
+            }
+
+            Ops removedOps = null;
+
+            // Remove any package state if such.
+            if (uidState.pkgOps != null) {
+                removedOps = uidState.pkgOps.remove(packageName);
+                mAppOpsCheckingService.removePackage(packageName, UserHandle.getUserId(uid));
+            }
+
+            // If we just nuked the last package state check if the UID is valid.
+            if (removedOps != null && uidState.pkgOps.isEmpty()
+                    && getPackagesForUid(uid).length <= 0) {
+                uidState.clear();
+                mUidStates.remove(uid);
+            }
+
+            if (removedOps != null) {
+                scheduleFastWriteLocked();
+
+                final int numOps = removedOps.size();
+                for (int opNum = 0; opNum < numOps; opNum++) {
+                    final Op op = removedOps.valueAt(opNum);
+
+                    final int numAttributions = op.mAttributions.size();
+                    for (int attributionNum = 0; attributionNum < numAttributions;
+                            attributionNum++) {
+                        AttributedOp attributedOp = op.mAttributions.valueAt(attributionNum);
+
+                        while (attributedOp.isRunning()) {
+                            attributedOp.finished(attributedOp.mInProgressEvents.keyAt(0));
+                        }
+                        while (attributedOp.isPaused()) {
+                            attributedOp.finished(attributedOp.mPausedInProgressEvents.keyAt(0));
+                        }
+                    }
+                }
+            }
+        }
+
+        mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory,
+                    mHistoricalRegistry, uid, packageName));
     }
 
-    /**
-     * Notify when a uid is removed.
-     */
     public void uidRemoved(int uid) {
-        mAppOpsService.uidRemoved(uid);
+        synchronized (this) {
+            if (mUidStates.indexOfKey(uid) >= 0) {
+                mUidStates.get(uid).clear();
+                mUidStates.remove(uid);
+                scheduleFastWriteLocked();
+            }
+        }
+    }
+
+    // The callback method from ForegroundPolicyInterface
+    private void onUidStateChanged(int uid, int state, boolean foregroundModeMayChange) {
+        synchronized (this) {
+            UidState uidState = getUidStateLocked(uid, true);
+
+            if (uidState != null && foregroundModeMayChange && uidState.hasForegroundWatchers) {
+                for (int fgi = uidState.foregroundOps.size() - 1; fgi >= 0; fgi--) {
+                    if (!uidState.foregroundOps.valueAt(fgi)) {
+                        continue;
+                    }
+                    final int code = uidState.foregroundOps.keyAt(fgi);
+
+                    if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)
+                            && uidState.getUidMode(code) == AppOpsManager.MODE_FOREGROUND) {
+                        mHandler.sendMessage(PooledLambda.obtainMessage(
+                                AppOpsService::notifyOpChangedForAllPkgsInUid,
+                                this, code, uidState.uid, true, null));
+                    } else if (uidState.pkgOps != null) {
+                        final ArraySet<OnOpModeChangedListener> listenerSet =
+                                mAppOpsCheckingService.getOpModeChangedListeners(code);
+                        if (listenerSet != null) {
+                            for (int cbi = listenerSet.size() - 1; cbi >= 0; cbi--) {
+                                final OnOpModeChangedListener listener = listenerSet.valueAt(cbi);
+                                if ((listener.getFlags()
+                                        & AppOpsManager.WATCH_FOREGROUND_CHANGES) == 0
+                                        || !listener.isWatchingUid(uidState.uid)) {
+                                    continue;
+                                }
+                                for (int pkgi = uidState.pkgOps.size() - 1; pkgi >= 0; pkgi--) {
+                                    final Op op = uidState.pkgOps.valueAt(pkgi).get(code);
+                                    if (op == null) {
+                                        continue;
+                                    }
+                                    if (op.getMode() == AppOpsManager.MODE_FOREGROUND) {
+                                        mHandler.sendMessage(PooledLambda.obtainMessage(
+                                                AppOpsService::notifyOpChanged,
+                                                this, listenerSet.valueAt(cbi), code, uidState.uid,
+                                                uidState.pkgOps.keyAt(pkgi)));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (uidState != null && uidState.pkgOps != null) {
+                int numPkgs = uidState.pkgOps.size();
+                for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
+                    Ops ops = uidState.pkgOps.valueAt(pkgNum);
+
+                    int numOps = ops.size();
+                    for (int opNum = 0; opNum < numOps; opNum++) {
+                        Op op = ops.valueAt(opNum);
+
+                        int numAttributions = op.mAttributions.size();
+                        for (int attributionNum = 0; attributionNum < numAttributions;
+                                attributionNum++) {
+                            AttributedOp attributedOp = op.mAttributions.valueAt(
+                                    attributionNum);
+
+                            attributedOp.onUidStateChanged(state);
+                        }
+                    }
+                }
+            }
+        }
     }
 
     /**
@@ -341,60 +1343,542 @@
      */
     public void updateUidProcState(int uid, int procState,
             @ActivityManager.ProcessCapability int capability) {
-        mAppOpsService.updateUidProcState(uid, procState, capability);
+        synchronized (this) {
+            getUidStateTracker().updateUidProcState(uid, procState, capability);
+            if (!mUidStates.contains(uid)) {
+                UidState uidState = new UidState(uid);
+                mUidStates.put(uid, uidState);
+                onUidStateChanged(uid,
+                        AppOpsUidStateTracker.processStateToUidState(procState), false);
+            }
+        }
     }
 
-    /**
-     * Initiates shutdown.
-     */
     public void shutdown() {
-        mAppOpsService.shutdown();
-
+        Slog.w(TAG, "Writing app ops before shutdown...");
+        boolean doWrite = false;
+        synchronized (this) {
+            if (mWriteScheduled) {
+                mWriteScheduled = false;
+                mFastWriteScheduled = false;
+                mHandler.removeCallbacks(mWriteRunner);
+                doWrite = true;
+            }
+        }
+        if (doWrite) {
+            writeState();
+        }
         if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED && mWriteNoteOpsScheduled) {
             writeNoteOps();
         }
+
+        mHistoricalRegistry.shutdown();
+    }
+
+    private ArrayList<AppOpsManager.OpEntry> collectOps(Ops pkgOps, int[] ops) {
+        ArrayList<AppOpsManager.OpEntry> resOps = null;
+        if (ops == null) {
+            resOps = new ArrayList<>();
+            for (int j=0; j<pkgOps.size(); j++) {
+                Op curOp = pkgOps.valueAt(j);
+                resOps.add(getOpEntryForResult(curOp));
+            }
+        } else {
+            for (int j=0; j<ops.length; j++) {
+                Op curOp = pkgOps.get(ops[j]);
+                if (curOp != null) {
+                    if (resOps == null) {
+                        resOps = new ArrayList<>();
+                    }
+                    resOps.add(getOpEntryForResult(curOp));
+                }
+            }
+        }
+        return resOps;
+    }
+
+    @Nullable
+    private ArrayList<AppOpsManager.OpEntry> collectUidOps(@NonNull UidState uidState,
+            @Nullable int[] ops) {
+        final SparseIntArray opModes = uidState.getNonDefaultUidModes();
+        if (opModes == null) {
+            return null;
+        }
+
+        int opModeCount = opModes.size();
+        if (opModeCount == 0) {
+            return null;
+        }
+        ArrayList<AppOpsManager.OpEntry> resOps = null;
+        if (ops == null) {
+            resOps = new ArrayList<>();
+            for (int i = 0; i < opModeCount; i++) {
+                int code = opModes.keyAt(i);
+                resOps.add(new OpEntry(code, opModes.get(code), Collections.emptyMap()));
+            }
+        } else {
+            for (int j=0; j<ops.length; j++) {
+                int code = ops[j];
+                if (opModes.indexOfKey(code) >= 0) {
+                    if (resOps == null) {
+                        resOps = new ArrayList<>();
+                    }
+                    resOps.add(new OpEntry(code, opModes.get(code), Collections.emptyMap()));
+                }
+            }
+        }
+        return resOps;
+    }
+
+    private static @NonNull OpEntry getOpEntryForResult(@NonNull Op op) {
+        return op.createEntryLocked();
     }
 
     @Override
     public List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops) {
-        return mAppOpsService.getPackagesForOps(ops);
+        final int callingUid = Binder.getCallingUid();
+        final boolean hasAllPackageAccess = mContext.checkPermission(
+                Manifest.permission.GET_APP_OPS_STATS, Binder.getCallingPid(),
+                Binder.getCallingUid(), null) == PackageManager.PERMISSION_GRANTED;
+        ArrayList<AppOpsManager.PackageOps> res = null;
+        synchronized (this) {
+            final int uidStateCount = mUidStates.size();
+            for (int i = 0; i < uidStateCount; i++) {
+                UidState uidState = mUidStates.valueAt(i);
+                if (uidState.pkgOps == null || uidState.pkgOps.isEmpty()) {
+                    continue;
+                }
+                ArrayMap<String, Ops> packages = uidState.pkgOps;
+                final int packageCount = packages.size();
+                for (int j = 0; j < packageCount; j++) {
+                    Ops pkgOps = packages.valueAt(j);
+                    ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops);
+                    if (resOps != null) {
+                        if (res == null) {
+                            res = new ArrayList<>();
+                        }
+                        AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
+                                pkgOps.packageName, pkgOps.uidState.uid, resOps);
+                        // Caller can always see their packages and with a permission all.
+                        if (hasAllPackageAccess || callingUid == pkgOps.uidState.uid) {
+                            res.add(resPackage);
+                        }
+                    }
+                }
+            }
+        }
+        return res;
     }
 
     @Override
     public List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName,
             int[] ops) {
-        return mAppOpsService.getOpsForPackage(uid, packageName, ops);
+        enforceGetAppOpsStatsPermissionIfNeeded(uid,packageName);
+        String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return Collections.emptyList();
+        }
+        synchronized (this) {
+            Ops pkgOps = getOpsLocked(uid, resolvedPackageName, null, false, null,
+                    /* edit */ false);
+            if (pkgOps == null) {
+                return null;
+            }
+            ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops);
+            if (resOps == null) {
+                return null;
+            }
+            ArrayList<AppOpsManager.PackageOps> res = new ArrayList<AppOpsManager.PackageOps>();
+            AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
+                    pkgOps.packageName, pkgOps.uidState.uid, resOps);
+            res.add(resPackage);
+            return res;
+        }
+    }
+
+    private void enforceGetAppOpsStatsPermissionIfNeeded(int uid, String packageName) {
+        final int callingUid = Binder.getCallingUid();
+        // We get to access everything
+        if (callingUid == Process.myPid()) {
+            return;
+        }
+        // Apps can access their own data
+        if (uid == callingUid && packageName != null
+                && checkPackage(uid, packageName) == MODE_ALLOWED) {
+            return;
+        }
+        // Otherwise, you need a permission...
+        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
+                Binder.getCallingPid(), callingUid, null);
+    }
+
+    /**
+     * Verify that historical appop request arguments are valid.
+     */
+    private void ensureHistoricalOpRequestIsValid(int uid, String packageName,
+            String attributionTag, List<String> opNames, int filter, long beginTimeMillis,
+            long endTimeMillis, int flags) {
+        if ((filter & FILTER_BY_UID) != 0) {
+            Preconditions.checkArgument(uid != Process.INVALID_UID);
+        } else {
+            Preconditions.checkArgument(uid == Process.INVALID_UID);
+        }
+
+        if ((filter & FILTER_BY_PACKAGE_NAME) != 0) {
+            Objects.requireNonNull(packageName);
+        } else {
+            Preconditions.checkArgument(packageName == null);
+        }
+
+        if ((filter & FILTER_BY_ATTRIBUTION_TAG) == 0) {
+            Preconditions.checkArgument(attributionTag == null);
+        }
+
+        if ((filter & FILTER_BY_OP_NAMES) != 0) {
+            Objects.requireNonNull(opNames);
+        } else {
+            Preconditions.checkArgument(opNames == null);
+        }
+
+        Preconditions.checkFlagsArgument(filter,
+                FILTER_BY_UID | FILTER_BY_PACKAGE_NAME | FILTER_BY_ATTRIBUTION_TAG
+                        | FILTER_BY_OP_NAMES);
+        Preconditions.checkArgumentNonnegative(beginTimeMillis);
+        Preconditions.checkArgument(endTimeMillis > beginTimeMillis);
+        Preconditions.checkFlagsArgument(flags, OP_FLAGS_ALL);
     }
 
     @Override
     public void getHistoricalOps(int uid, String packageName, String attributionTag,
             List<String> opNames, int dataType, int filter, long beginTimeMillis,
             long endTimeMillis, int flags, RemoteCallback callback) {
-        mAppOpsService.getHistoricalOps(uid, packageName, attributionTag, opNames,
-                dataType, filter, beginTimeMillis, endTimeMillis, flags, callback);
+        PackageManager pm = mContext.getPackageManager();
+
+        ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter,
+                beginTimeMillis, endTimeMillis, flags);
+        Objects.requireNonNull(callback, "callback cannot be null");
+        ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class);
+        boolean isSelfRequest = (filter & FILTER_BY_UID) != 0 && uid == Binder.getCallingUid();
+        if (!isSelfRequest) {
+            boolean isCallerInstrumented =
+                    ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID;
+            boolean isCallerSystem = Binder.getCallingPid() == Process.myPid();
+            boolean isCallerPermissionController;
+            try {
+                isCallerPermissionController = pm.getPackageUidAsUser(
+                        mContext.getPackageManager().getPermissionControllerPackageName(), 0,
+                        UserHandle.getUserId(Binder.getCallingUid()))
+                        == Binder.getCallingUid();
+            } catch (PackageManager.NameNotFoundException doesNotHappen) {
+                return;
+            }
+
+            boolean doesCallerHavePermission = mContext.checkPermission(
+                    android.Manifest.permission.GET_HISTORICAL_APP_OPS_STATS,
+                    Binder.getCallingPid(), Binder.getCallingUid())
+                    == PackageManager.PERMISSION_GRANTED;
+
+            if (!isCallerSystem && !isCallerInstrumented && !isCallerPermissionController
+                    && !doesCallerHavePermission) {
+                mHandler.post(() -> callback.sendResult(new Bundle()));
+                return;
+            }
+
+            mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
+                    Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps");
+        }
+
+        final String[] opNamesArray = (opNames != null)
+                ? opNames.toArray(new String[opNames.size()]) : null;
+
+        Set<String> attributionChainExemptPackages = null;
+        if ((dataType & HISTORY_FLAG_GET_ATTRIBUTION_CHAINS) != 0) {
+            attributionChainExemptPackages =
+                    PermissionManager.getIndicatorExemptedPackages(mContext);
+        }
+
+        final String[] chainExemptPkgArray = attributionChainExemptPackages != null
+                ? attributionChainExemptPackages.toArray(
+                        new String[attributionChainExemptPackages.size()]) : null;
+
+        // Must not hold the appops lock
+        mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps,
+                mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType,
+                filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray,
+                callback).recycleOnUse());
     }
 
     @Override
     public void getHistoricalOpsFromDiskRaw(int uid, String packageName, String attributionTag,
             List<String> opNames, int dataType, int filter, long beginTimeMillis,
             long endTimeMillis, int flags, RemoteCallback callback) {
-        mAppOpsService.getHistoricalOpsFromDiskRaw(uid, packageName, attributionTag,
-                opNames, dataType, filter, beginTimeMillis, endTimeMillis, flags, callback);
+        ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter,
+                beginTimeMillis, endTimeMillis, flags);
+        Objects.requireNonNull(callback, "callback cannot be null");
+
+        mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS,
+                Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps");
+
+        final String[] opNamesArray = (opNames != null)
+                ? opNames.toArray(new String[opNames.size()]) : null;
+
+        Set<String> attributionChainExemptPackages = null;
+        if ((dataType & HISTORY_FLAG_GET_ATTRIBUTION_CHAINS) != 0) {
+            attributionChainExemptPackages =
+                    PermissionManager.getIndicatorExemptedPackages(mContext);
+        }
+
+        final String[] chainExemptPkgArray = attributionChainExemptPackages != null
+                ? attributionChainExemptPackages.toArray(
+                new String[attributionChainExemptPackages.size()]) : null;
+
+        // Must not hold the appops lock
+        mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOpsFromDiskRaw,
+                mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType,
+                filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray,
+                callback).recycleOnUse());
     }
 
     @Override
     public void reloadNonHistoricalState() {
-        mAppOpsService.reloadNonHistoricalState();
+        mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS,
+                Binder.getCallingPid(), Binder.getCallingUid(), "reloadNonHistoricalState");
+        writeState();
+        readState();
     }
 
     @Override
     public List<AppOpsManager.PackageOps> getUidOps(int uid, int[] ops) {
-        return mAppOpsService.getUidOps(uid, ops);
+        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
+                Binder.getCallingPid(), Binder.getCallingUid(), null);
+        synchronized (this) {
+            UidState uidState = getUidStateLocked(uid, false);
+            if (uidState == null) {
+                return null;
+            }
+            ArrayList<AppOpsManager.OpEntry> resOps = collectUidOps(uidState, ops);
+            if (resOps == null) {
+                return null;
+            }
+            ArrayList<AppOpsManager.PackageOps> res = new ArrayList<AppOpsManager.PackageOps>();
+            AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
+                    null, uidState.uid, resOps);
+            res.add(resPackage);
+            return res;
+        }
+    }
+
+    private void pruneOpLocked(Op op, int uid, String packageName) {
+        op.removeAttributionsWithNoTime();
+
+        if (op.mAttributions.isEmpty()) {
+            Ops ops = getOpsLocked(uid, packageName, null, false, null, /* edit */ false);
+            if (ops != null) {
+                ops.remove(op.op);
+                op.setMode(AppOpsManager.opToDefaultMode(op.op));
+                if (ops.size() <= 0) {
+                    UidState uidState = ops.uidState;
+                    ArrayMap<String, Ops> pkgOps = uidState.pkgOps;
+                    if (pkgOps != null) {
+                        pkgOps.remove(ops.packageName);
+                        mAppOpsCheckingService.removePackage(ops.packageName,
+                                UserHandle.getUserId(uidState.uid));
+                        if (pkgOps.isEmpty()) {
+                            uidState.pkgOps = null;
+                        }
+                        if (uidState.isDefault()) {
+                            uidState.clear();
+                            mUidStates.remove(uid);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void enforceManageAppOpsModes(int callingPid, int callingUid, int targetUid) {
+        if (callingPid == Process.myPid()) {
+            return;
+        }
+        final int callingUser = UserHandle.getUserId(callingUid);
+        synchronized (this) {
+            if (mProfileOwners != null && mProfileOwners.get(callingUser, -1) == callingUid) {
+                if (targetUid >= 0 && callingUser == UserHandle.getUserId(targetUid)) {
+                    // Profile owners are allowed to change modes but only for apps
+                    // within their user.
+                    return;
+                }
+            }
+        }
+        mContext.enforcePermission(android.Manifest.permission.MANAGE_APP_OPS_MODES,
+                Binder.getCallingPid(), Binder.getCallingUid(), null);
     }
 
     @Override
     public void setUidMode(int code, int uid, int mode) {
-        mAppOpsService.setUidMode(code, uid, mode, null);
+        setUidMode(code, uid, mode, null);
+    }
+
+    private void setUidMode(int code, int uid, int mode,
+            @Nullable IAppOpsCallback permissionPolicyCallback) {
+        if (DEBUG) {
+            Slog.i(TAG, "uid " + uid + " OP_" + opToName(code) + " := " + modeToName(mode)
+                    + " by uid " + Binder.getCallingUid());
+        }
+
+        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid);
+        verifyIncomingOp(code);
+        code = AppOpsManager.opToSwitch(code);
+
+        if (permissionPolicyCallback == null) {
+            updatePermissionRevokedCompat(uid, code, mode);
+        }
+
+        int previousMode;
+        synchronized (this) {
+            final int defaultMode = AppOpsManager.opToDefaultMode(code);
+
+            UidState uidState = getUidStateLocked(uid, false);
+            if (uidState == null) {
+                if (mode == defaultMode) {
+                    return;
+                }
+                uidState = new UidState(uid);
+                mUidStates.put(uid, uidState);
+            }
+            if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) {
+                previousMode = uidState.getUidMode(code);
+            } else {
+                // doesn't look right but is legacy behavior.
+                previousMode = MODE_DEFAULT;
+            }
+
+            if (!uidState.setUidMode(code, mode)) {
+                return;
+            }
+            uidState.evalForegroundOps();
+            if (mode != MODE_ERRORED && mode != previousMode) {
+                updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
+            }
+        }
+
+        notifyOpChangedForAllPkgsInUid(code, uid, false, permissionPolicyCallback);
+        notifyOpChangedSync(code, uid, null, mode, previousMode);
+    }
+
+    /**
+     * Notify that an op changed for all packages in an uid.
+     *
+     * @param code The op that changed
+     * @param uid The uid the op was changed for
+     * @param onlyForeground Only notify watchers that watch for foreground changes
+     */
+    private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground,
+            @Nullable IAppOpsCallback callbackToIgnore) {
+        ModeCallback listenerToIgnore = callbackToIgnore != null
+                ? mModeWatchers.get(callbackToIgnore.asBinder()) : null;
+        mAppOpsCheckingService.notifyOpChangedForAllPkgsInUid(code, uid, onlyForeground,
+                listenerToIgnore);
+    }
+
+    private void updatePermissionRevokedCompat(int uid, int switchCode, int mode) {
+        PackageManager packageManager = mContext.getPackageManager();
+        if (packageManager == null) {
+            // This can only happen during early boot. At this time the permission state and appop
+            // state are in sync
+            return;
+        }
+
+        String[] packageNames = packageManager.getPackagesForUid(uid);
+        if (ArrayUtils.isEmpty(packageNames)) {
+            return;
+        }
+        String packageName = packageNames[0];
+
+        int[] ops = mSwitchedOps.get(switchCode);
+        for (int code : ops) {
+            String permissionName = AppOpsManager.opToPermission(code);
+            if (permissionName == null) {
+                continue;
+            }
+
+            if (packageManager.checkPermission(permissionName, packageName)
+                    != PackageManager.PERMISSION_GRANTED) {
+                continue;
+            }
+
+            PermissionInfo permissionInfo;
+            try {
+                permissionInfo = packageManager.getPermissionInfo(permissionName, 0);
+            } catch (PackageManager.NameNotFoundException e) {
+                e.printStackTrace();
+                continue;
+            }
+
+            if (!permissionInfo.isRuntime()) {
+                continue;
+            }
+
+            boolean supportsRuntimePermissions = getPackageManagerInternal()
+                    .getUidTargetSdkVersion(uid) >= Build.VERSION_CODES.M;
+
+            UserHandle user = UserHandle.getUserHandleForUid(uid);
+            boolean isRevokedCompat;
+            if (permissionInfo.backgroundPermission != null) {
+                if (packageManager.checkPermission(permissionInfo.backgroundPermission, packageName)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    boolean isBackgroundRevokedCompat = mode != AppOpsManager.MODE_ALLOWED;
+
+                    if (isBackgroundRevokedCompat && supportsRuntimePermissions) {
+                        Slog.w(TAG, "setUidMode() called with a mode inconsistent with runtime"
+                                + " permission state, this is discouraged and you should revoke the"
+                                + " runtime permission instead: uid=" + uid + ", switchCode="
+                                + switchCode + ", mode=" + mode + ", permission="
+                                + permissionInfo.backgroundPermission);
+                    }
+
+                    final long identity = Binder.clearCallingIdentity();
+                    try {
+                        packageManager.updatePermissionFlags(permissionInfo.backgroundPermission,
+                                packageName, PackageManager.FLAG_PERMISSION_REVOKED_COMPAT,
+                                isBackgroundRevokedCompat
+                                        ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0, user);
+                    } finally {
+                        Binder.restoreCallingIdentity(identity);
+                    }
+                }
+
+                isRevokedCompat = mode != AppOpsManager.MODE_ALLOWED
+                        && mode != AppOpsManager.MODE_FOREGROUND;
+            } else {
+                isRevokedCompat = mode != AppOpsManager.MODE_ALLOWED;
+            }
+
+            if (isRevokedCompat && supportsRuntimePermissions) {
+                Slog.w(TAG, "setUidMode() called with a mode inconsistent with runtime"
+                        + " permission state, this is discouraged and you should revoke the"
+                        + " runtime permission instead: uid=" + uid + ", switchCode="
+                        + switchCode + ", mode=" + mode + ", permission=" + permissionName);
+            }
+
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                packageManager.updatePermissionFlags(permissionName, packageName,
+                        PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, isRevokedCompat
+                                ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0, user);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    private void notifyOpChangedSync(int code, int uid, @NonNull String packageName, int mode,
+            int previousMode) {
+        final StorageManagerInternal storageManagerInternal =
+                LocalServices.getService(StorageManagerInternal.class);
+        if (storageManagerInternal != null) {
+            storageManagerInternal.onAppOpsChanged(code, uid, packageName, mode, previousMode);
+        }
     }
 
     /**
@@ -407,12 +1891,309 @@
      */
     @Override
     public void setMode(int code, int uid, @NonNull String packageName, int mode) {
-        mAppOpsService.setMode(code, uid, packageName, mode, null);
+        setMode(code, uid, packageName, mode, null);
+    }
+
+    void setMode(int code, int uid, @NonNull String packageName, int mode,
+            @Nullable IAppOpsCallback permissionPolicyCallback) {
+        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid);
+        verifyIncomingOp(code);
+        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
+            return;
+        }
+
+        ArraySet<OnOpModeChangedListener> repCbs = null;
+        code = AppOpsManager.opToSwitch(code);
+
+        PackageVerificationResult pvr;
+        try {
+            pvr = verifyAndGetBypass(uid, packageName, null);
+        } catch (SecurityException e) {
+            Slog.e(TAG, "Cannot setMode", e);
+            return;
+        }
+
+        int previousMode = MODE_DEFAULT;
+        synchronized (this) {
+            UidState uidState = getUidStateLocked(uid, false);
+            Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ true);
+            if (op != null) {
+                if (op.getMode() != mode) {
+                    previousMode = op.getMode();
+                    op.setMode(mode);
+
+                    if (uidState != null) {
+                        uidState.evalForegroundOps();
+                    }
+                    ArraySet<OnOpModeChangedListener> cbs =
+                            mAppOpsCheckingService.getOpModeChangedListeners(code);
+                    if (cbs != null) {
+                        if (repCbs == null) {
+                            repCbs = new ArraySet<>();
+                        }
+                        repCbs.addAll(cbs);
+                    }
+                    cbs = mAppOpsCheckingService.getPackageModeChangedListeners(packageName);
+                    if (cbs != null) {
+                        if (repCbs == null) {
+                            repCbs = new ArraySet<>();
+                        }
+                        repCbs.addAll(cbs);
+                    }
+                    if (repCbs != null && permissionPolicyCallback != null) {
+                        repCbs.remove(mModeWatchers.get(permissionPolicyCallback.asBinder()));
+                    }
+                    if (mode == AppOpsManager.opToDefaultMode(op.op)) {
+                        // If going into the default mode, prune this op
+                        // if there is nothing else interesting in it.
+                        pruneOpLocked(op, uid, packageName);
+                    }
+                    scheduleFastWriteLocked();
+                    if (mode != MODE_ERRORED) {
+                        updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
+                    }
+                }
+            }
+        }
+        if (repCbs != null) {
+            mHandler.sendMessage(PooledLambda.obtainMessage(
+                    AppOpsService::notifyOpChanged,
+                    this, repCbs, code, uid, packageName));
+        }
+
+        notifyOpChangedSync(code, uid, packageName, mode, previousMode);
+    }
+
+    private void notifyOpChanged(ArraySet<OnOpModeChangedListener> callbacks, int code,
+            int uid, String packageName) {
+        for (int i = 0; i < callbacks.size(); i++) {
+            final OnOpModeChangedListener callback = callbacks.valueAt(i);
+            notifyOpChanged(callback, code, uid, packageName);
+        }
+    }
+
+    private void notifyOpChanged(OnOpModeChangedListener callback, int code,
+            int uid, String packageName) {
+        mAppOpsCheckingService.notifyOpChanged(callback, code, uid, packageName);
+    }
+
+    private static ArrayList<ChangeRec> addChange(ArrayList<ChangeRec> reports,
+            int op, int uid, String packageName, int previousMode) {
+        boolean duplicate = false;
+        if (reports == null) {
+            reports = new ArrayList<>();
+        } else {
+            final int reportCount = reports.size();
+            for (int j = 0; j < reportCount; j++) {
+                ChangeRec report = reports.get(j);
+                if (report.op == op && report.pkg.equals(packageName)) {
+                    duplicate = true;
+                    break;
+                }
+            }
+        }
+        if (!duplicate) {
+            reports.add(new ChangeRec(op, uid, packageName, previousMode));
+        }
+
+        return reports;
+    }
+
+    private static HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> addCallbacks(
+            HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks,
+            int op, int uid, String packageName, int previousMode,
+            ArraySet<OnOpModeChangedListener> cbs) {
+        if (cbs == null) {
+            return callbacks;
+        }
+        if (callbacks == null) {
+            callbacks = new HashMap<>();
+        }
+        final int N = cbs.size();
+        for (int i=0; i<N; i++) {
+            OnOpModeChangedListener cb = cbs.valueAt(i);
+            ArrayList<ChangeRec> reports = callbacks.get(cb);
+            ArrayList<ChangeRec> changed = addChange(reports, op, uid, packageName, previousMode);
+            if (changed != reports) {
+                callbacks.put(cb, changed);
+            }
+        }
+        return callbacks;
+    }
+
+    static final class ChangeRec {
+        final int op;
+        final int uid;
+        final String pkg;
+        final int previous_mode;
+
+        ChangeRec(int _op, int _uid, String _pkg, int _previous_mode) {
+            op = _op;
+            uid = _uid;
+            pkg = _pkg;
+            previous_mode = _previous_mode;
+        }
     }
 
     @Override
     public void resetAllModes(int reqUserId, String reqPackageName) {
-        mAppOpsService.resetAllModes(reqUserId, reqPackageName);
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
+        reqUserId = ActivityManager.handleIncomingUser(callingPid, callingUid, reqUserId,
+                true, true, "resetAllModes", null);
+
+        int reqUid = -1;
+        if (reqPackageName != null) {
+            try {
+                reqUid = AppGlobals.getPackageManager().getPackageUid(
+                        reqPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, reqUserId);
+            } catch (RemoteException e) {
+                /* ignore - local call */
+            }
+        }
+
+        enforceManageAppOpsModes(callingPid, callingUid, reqUid);
+
+        HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks = null;
+        ArrayList<ChangeRec> allChanges = new ArrayList<>();
+        synchronized (this) {
+            boolean changed = false;
+            for (int i = mUidStates.size() - 1; i >= 0; i--) {
+                UidState uidState = mUidStates.valueAt(i);
+
+                SparseIntArray opModes = uidState.getNonDefaultUidModes();
+                if (opModes != null && (uidState.uid == reqUid || reqUid == -1)) {
+                    final int uidOpCount = opModes.size();
+                    for (int j = uidOpCount - 1; j >= 0; j--) {
+                        final int code = opModes.keyAt(j);
+                        if (AppOpsManager.opAllowsReset(code)) {
+                            int previousMode = opModes.valueAt(j);
+                            uidState.setUidMode(code, AppOpsManager.opToDefaultMode(code));
+                            for (String packageName : getPackagesForUid(uidState.uid)) {
+                                callbacks = addCallbacks(callbacks, code, uidState.uid, packageName,
+                                        previousMode,
+                                        mAppOpsCheckingService.getOpModeChangedListeners(code));
+                                callbacks = addCallbacks(callbacks, code, uidState.uid, packageName,
+                                        previousMode, mAppOpsCheckingService
+                                                .getPackageModeChangedListeners(packageName));
+
+                                allChanges = addChange(allChanges, code, uidState.uid,
+                                        packageName, previousMode);
+                            }
+                        }
+                    }
+                }
+
+                if (uidState.pkgOps == null) {
+                    continue;
+                }
+
+                if (reqUserId != UserHandle.USER_ALL
+                        && reqUserId != UserHandle.getUserId(uidState.uid)) {
+                    // Skip any ops for a different user
+                    continue;
+                }
+
+                Map<String, Ops> packages = uidState.pkgOps;
+                Iterator<Map.Entry<String, Ops>> it = packages.entrySet().iterator();
+                boolean uidChanged = false;
+                while (it.hasNext()) {
+                    Map.Entry<String, Ops> ent = it.next();
+                    String packageName = ent.getKey();
+                    if (reqPackageName != null && !reqPackageName.equals(packageName)) {
+                        // Skip any ops for a different package
+                        continue;
+                    }
+                    Ops pkgOps = ent.getValue();
+                    for (int j=pkgOps.size()-1; j>=0; j--) {
+                        Op curOp = pkgOps.valueAt(j);
+                        if (shouldDeferResetOpToDpm(curOp.op)) {
+                            deferResetOpToDpm(curOp.op, reqPackageName, reqUserId);
+                            continue;
+                        }
+                        if (AppOpsManager.opAllowsReset(curOp.op)
+                                && curOp.getMode() != AppOpsManager.opToDefaultMode(curOp.op)) {
+                            int previousMode = curOp.getMode();
+                            curOp.setMode(AppOpsManager.opToDefaultMode(curOp.op));
+                            changed = true;
+                            uidChanged = true;
+                            final int uid = curOp.uidState.uid;
+                            callbacks = addCallbacks(callbacks, curOp.op, uid, packageName,
+                                    previousMode,
+                                    mAppOpsCheckingService.getOpModeChangedListeners(curOp.op));
+                            callbacks = addCallbacks(callbacks, curOp.op, uid, packageName,
+                                    previousMode, mAppOpsCheckingService
+                                            .getPackageModeChangedListeners(packageName));
+
+                            allChanges = addChange(allChanges, curOp.op, uid, packageName,
+                                    previousMode);
+                            curOp.removeAttributionsWithNoTime();
+                            if (curOp.mAttributions.isEmpty()) {
+                                pkgOps.removeAt(j);
+                            }
+                        }
+                    }
+                    if (pkgOps.size() == 0) {
+                        it.remove();
+                        mAppOpsCheckingService.removePackage(packageName,
+                                UserHandle.getUserId(uidState.uid));
+                    }
+                }
+                if (uidState.isDefault()) {
+                    uidState.clear();
+                    mUidStates.remove(uidState.uid);
+                }
+                if (uidChanged) {
+                    uidState.evalForegroundOps();
+                }
+            }
+
+            if (changed) {
+                scheduleFastWriteLocked();
+            }
+        }
+        if (callbacks != null) {
+            for (Map.Entry<OnOpModeChangedListener, ArrayList<ChangeRec>> ent
+                    : callbacks.entrySet()) {
+                OnOpModeChangedListener cb = ent.getKey();
+                ArrayList<ChangeRec> reports = ent.getValue();
+                for (int i=0; i<reports.size(); i++) {
+                    ChangeRec rep = reports.get(i);
+                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                            AppOpsService::notifyOpChanged,
+                            this, cb, rep.op, rep.uid, rep.pkg));
+                }
+            }
+        }
+
+        int numChanges = allChanges.size();
+        for (int i = 0; i < numChanges; i++) {
+            ChangeRec change = allChanges.get(i);
+            notifyOpChangedSync(change.op, change.uid, change.pkg,
+                    AppOpsManager.opToDefaultMode(change.op), change.previous_mode);
+        }
+    }
+
+    private boolean shouldDeferResetOpToDpm(int op) {
+        // TODO(b/174582385): avoid special-casing app-op resets by migrating app-op permission
+        //  pre-grants to a role-based mechanism or another general-purpose mechanism.
+        return dpmi != null && dpmi.supportsResetOp(op);
+    }
+
+    /** Assumes {@link #shouldDeferResetOpToDpm(int)} is true. */
+    private void deferResetOpToDpm(int op, String packageName, @UserIdInt int userId) {
+        // TODO(b/174582385): avoid special-casing app-op resets by migrating app-op permission
+        //  pre-grants to a role-based mechanism or another general-purpose mechanism.
+        dpmi.resetOp(op, packageName, userId);
+    }
+
+    private void evalAllForegroundOpsLocked() {
+        for (int uidi = mUidStates.size() - 1; uidi >= 0; uidi--) {
+            final UidState uidState = mUidStates.valueAt(uidi);
+            if (uidState.foregroundOps != null) {
+                uidState.evalForegroundOps();
+            }
+        }
     }
 
     @Override
@@ -423,17 +2204,66 @@
     @Override
     public void startWatchingModeWithFlags(int op, String packageName, int flags,
             IAppOpsCallback callback) {
-        mAppOpsService.startWatchingModeWithFlags(op, packageName, flags, callback);
+        int watchedUid = -1;
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        // TODO: should have a privileged permission to protect this.
+        // Also, if the caller has requested WATCH_FOREGROUND_CHANGES, should we require
+        // the USAGE_STATS permission since this can provide information about when an
+        // app is in the foreground?
+        Preconditions.checkArgumentInRange(op, AppOpsManager.OP_NONE,
+                AppOpsManager._NUM_OP - 1, "Invalid op code: " + op);
+        if (callback == null) {
+            return;
+        }
+        final boolean mayWatchPackageName = packageName != null
+                && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(callingUid));
+        synchronized (this) {
+            int switchOp = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op;
+
+            int notifiedOps;
+            if ((flags & CALL_BACK_ON_SWITCHED_OP) == 0) {
+                if (op == OP_NONE) {
+                    notifiedOps = ALL_OPS;
+                } else {
+                    notifiedOps = op;
+                }
+            } else {
+                notifiedOps = switchOp;
+            }
+
+            ModeCallback cb = mModeWatchers.get(callback.asBinder());
+            if (cb == null) {
+                cb = new ModeCallback(callback, watchedUid, flags, notifiedOps, callingUid,
+                        callingPid);
+                mModeWatchers.put(callback.asBinder(), cb);
+            }
+            if (switchOp != AppOpsManager.OP_NONE) {
+                mAppOpsCheckingService.startWatchingOpModeChanged(cb, switchOp);
+            }
+            if (mayWatchPackageName) {
+                mAppOpsCheckingService.startWatchingPackageModeChanged(cb, packageName);
+            }
+            evalAllForegroundOpsLocked();
+        }
     }
 
     @Override
     public void stopWatchingMode(IAppOpsCallback callback) {
-        mAppOpsService.stopWatchingMode(callback);
+        if (callback == null) {
+            return;
+        }
+        synchronized (this) {
+            ModeCallback cb = mModeWatchers.remove(callback.asBinder());
+            if (cb != null) {
+                cb.unlinkToDeath();
+                mAppOpsCheckingService.removeListener(cb);
+            }
+
+            evalAllForegroundOpsLocked();
+        }
     }
 
-    /**
-     * @return the current {@link CheckOpsDelegate}.
-     */
     public CheckOpsDelegate getAppOpsServiceDelegate() {
         synchronized (AppOpsService.this) {
             final CheckOpsDelegateDispatcher dispatcher = mCheckOpsDelegateDispatcher;
@@ -441,9 +2271,6 @@
         }
     }
 
-    /**
-     * Sets the appops {@link CheckOpsDelegate}
-     */
     public void setAppOpsServiceDelegate(CheckOpsDelegate delegate) {
         synchronized (AppOpsService.this) {
             final CheckOpsDelegateDispatcher oldDispatcher = mCheckOpsDelegateDispatcher;
@@ -467,7 +2294,58 @@
 
     private int checkOperationImpl(int code, int uid, String packageName,
             @Nullable String attributionTag, boolean raw) {
-        return mAppOpsService.checkOperation(code, uid, packageName, attributionTag, raw);
+        verifyIncomingOp(code);
+        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
+            return AppOpsManager.opToDefaultMode(code);
+        }
+
+        String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return AppOpsManager.MODE_IGNORED;
+        }
+        return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag, raw);
+    }
+
+    /**
+     * Get the mode of an app-op.
+     *
+     * @param code The code of the op
+     * @param uid The uid of the package the op belongs to
+     * @param packageName The package the op belongs to
+     * @param raw If the raw state of eval-ed state should be checked.
+     *
+     * @return The mode of the op
+     */
+    private @Mode int checkOperationUnchecked(int code, int uid, @NonNull String packageName,
+            @Nullable String attributionTag, boolean raw) {
+        PackageVerificationResult pvr;
+        try {
+            pvr = verifyAndGetBypass(uid, packageName, null);
+        } catch (SecurityException e) {
+            Slog.e(TAG, "checkOperation", e);
+            return AppOpsManager.opToDefaultMode(code);
+        }
+
+        if (isOpRestrictedDueToSuspend(code, packageName, uid)) {
+            return AppOpsManager.MODE_IGNORED;
+        }
+        synchronized (this) {
+            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, true)) {
+                return AppOpsManager.MODE_IGNORED;
+            }
+            code = AppOpsManager.opToSwitch(code);
+            UidState uidState = getUidStateLocked(uid, false);
+            if (uidState != null
+                    && uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) {
+                final int rawMode = uidState.getUidMode(code);
+                return raw ? rawMode : uidState.evalMode(code, rawMode);
+            }
+            Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false);
+            if (op == null) {
+                return AppOpsManager.opToDefaultMode(code);
+            }
+            return raw ? op.getMode() : op.uidState.evalMode(op.op, op.getMode());
+        }
     }
 
     @Override
@@ -487,8 +2365,7 @@
     @Override
     public void setAudioRestriction(int code, int usage, int uid, int mode,
             String[] exceptionPackages) {
-        mAppOpsService.enforceManageAppOpsModes(Binder.getCallingPid(),
-                Binder.getCallingUid(), uid);
+        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid);
         verifyIncomingUid(uid);
         verifyIncomingOp(code);
 
@@ -496,35 +2373,58 @@
                 code, usage, uid, mode, exceptionPackages);
 
         mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsServiceInterface::notifyWatchersOfChange, mAppOpsService, code,
-                UID_ANY));
+                AppOpsService::notifyWatchersOfChange, this, code, UID_ANY));
     }
 
 
     @Override
     public void setCameraAudioRestriction(@CAMERA_AUDIO_RESTRICTION int mode) {
-        mAppOpsService.enforceManageAppOpsModes(Binder.getCallingPid(),
-                Binder.getCallingUid(), -1);
+        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), -1);
 
         mAudioRestrictionManager.setCameraAudioRestriction(mode);
 
         mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsServiceInterface::notifyWatchersOfChange, mAppOpsService,
+                AppOpsService::notifyWatchersOfChange, this,
                 AppOpsManager.OP_PLAY_AUDIO, UID_ANY));
         mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsServiceInterface::notifyWatchersOfChange, mAppOpsService,
+                AppOpsService::notifyWatchersOfChange, this,
                 AppOpsManager.OP_VIBRATE, UID_ANY));
     }
 
     @Override
     public int checkPackage(int uid, String packageName) {
-        return mAppOpsService.checkPackage(uid, packageName);
+        Objects.requireNonNull(packageName);
+        try {
+            verifyAndGetBypass(uid, packageName, null);
+            // When the caller is the system, it's possible that the packageName is the special
+            // one (e.g., "root") which isn't actually existed.
+            if (resolveUid(packageName) == uid
+                    || (isPackageExisted(packageName)
+                            && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(uid)))) {
+                return AppOpsManager.MODE_ALLOWED;
+            }
+            return AppOpsManager.MODE_ERRORED;
+        } catch (SecurityException ignored) {
+            return AppOpsManager.MODE_ERRORED;
+        }
     }
 
     private boolean isPackageExisted(String packageName) {
         return getPackageManagerInternal().getPackageStateInternal(packageName) != null;
     }
 
+    /**
+     * This method will check with PackageManager to determine if the package provided should
+     * be visible to the {@link Binder#getCallingUid()}.
+     *
+     * NOTE: This must not be called while synchronized on {@code this} to avoid dead locks
+     */
+    private boolean filterAppAccessUnlocked(String packageName, int userId) {
+        final int callingUid = Binder.getCallingUid();
+        return LocalServices.getService(PackageManagerInternal.class)
+                .filterAppAccess(packageName, callingUid, userId);
+    }
+
     @Override
     public SyncNotedAppOp noteProxyOperation(int code, AttributionSource attributionSource,
             boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
@@ -570,20 +2470,13 @@
             final int proxyFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXY
                     : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY;
 
-            final int proxyReturn = mAppOpsService.noteOperationUnchecked(code, proxyUid,
+            final SyncNotedAppOp proxyReturn = noteOperationUnchecked(code, proxyUid,
                     resolveProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null,
-                    proxyFlags);
-            if (proxyReturn != AppOpsManager.MODE_ALLOWED) {
-                return new SyncNotedAppOp(proxyReturn, code, proxiedAttributionTag,
+                    proxyFlags, !isProxyTrusted, "proxy " + message, shouldCollectMessage);
+            if (proxyReturn.getOpMode() != AppOpsManager.MODE_ALLOWED) {
+                return new SyncNotedAppOp(proxyReturn.getOpMode(), code, proxiedAttributionTag,
                         proxiedPackageName);
             }
-            if (shouldCollectAsyncNotedOp) {
-                boolean isProxyAttributionTagValid = mAppOpsService.isAttributionTagValid(proxyUid,
-                        resolveProxyPackageName, proxyAttributionTag, null);
-                collectAsyncNotedOp(proxyUid, resolveProxyPackageName, code,
-                        isProxyAttributionTagValid ? proxyAttributionTag : null, proxyFlags,
-                        message, shouldCollectMessage);
-            }
         }
 
         String resolveProxiedPackageName = AppOpsManager.resolvePackageName(proxiedUid,
@@ -595,32 +2488,9 @@
 
         final int proxiedFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXIED
                 : AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED;
-        final int result = mAppOpsService.noteOperationUnchecked(code, proxiedUid,
-                resolveProxiedPackageName, proxiedAttributionTag, proxyUid, resolveProxyPackageName,
-                proxyAttributionTag, proxiedFlags);
-
-        boolean isProxiedAttributionTagValid = mAppOpsService.isAttributionTagValid(proxiedUid,
-                resolveProxiedPackageName, proxiedAttributionTag, resolveProxyPackageName);
-        if (shouldCollectAsyncNotedOp && result == AppOpsManager.MODE_ALLOWED) {
-            collectAsyncNotedOp(proxiedUid, resolveProxiedPackageName, code,
-                    isProxiedAttributionTagValid ? proxiedAttributionTag : null, proxiedFlags,
-                    message, shouldCollectMessage);
-        }
-
-
-        return new SyncNotedAppOp(result, code,
-                isProxiedAttributionTagValid ? proxiedAttributionTag : null,
-                resolveProxiedPackageName);
-    }
-
-    private boolean isCallerAndAttributionTrusted(@NonNull AttributionSource attributionSource) {
-        if (attributionSource.getUid() != Binder.getCallingUid()
-                && attributionSource.isTrusted(mContext)) {
-            return true;
-        }
-        return mContext.checkPermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
-                Binder.getCallingPid(), Binder.getCallingUid(), null)
-                == PackageManager.PERMISSION_GRANTED;
+        return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
+                proxiedAttributionTag, proxyUid, resolveProxyPackageName, proxyAttributionTag,
+                proxiedFlags, shouldCollectAsyncNotedOp, message, shouldCollectMessage);
     }
 
     @Override
@@ -634,58 +2504,258 @@
     private SyncNotedAppOp noteOperationImpl(int code, int uid, @Nullable String packageName,
             @Nullable String attributionTag, boolean shouldCollectAsyncNotedOp,
             @Nullable String message, boolean shouldCollectMessage) {
+        verifyIncomingUid(uid);
+        verifyIncomingOp(code);
         if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
                     packageName);
         }
 
-        int result = mAppOpsService.noteOperation(code, uid, packageName,
-                attributionTag, message);
-
         String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
+                    packageName);
+        }
+        return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
+                Process.INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF,
+                shouldCollectAsyncNotedOp, message, shouldCollectMessage);
+    }
 
-        boolean isAttributionTagValid = mAppOpsService.isAttributionTagValid(uid,
-                    resolvedPackageName, attributionTag, null);
-
-        if (shouldCollectAsyncNotedOp && result == MODE_ALLOWED) {
-            collectAsyncNotedOp(uid, resolvedPackageName, code,
-                    isAttributionTagValid ? attributionTag : null, AppOpsManager.OP_FLAG_SELF,
-                    message, shouldCollectMessage);
+    private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName,
+            @Nullable String attributionTag, int proxyUid, String proxyPackageName,
+            @Nullable String proxyAttributionTag, @OpFlags int flags,
+            boolean shouldCollectAsyncNotedOp, @Nullable String message,
+            boolean shouldCollectMessage) {
+        PackageVerificationResult pvr;
+        try {
+            pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
+            boolean wasNull = attributionTag == null;
+            if (!pvr.isAttributionTagValid) {
+                attributionTag = null;
+            }
+        } catch (SecurityException e) {
+            Slog.e(TAG, "noteOperation", e);
+            return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
+                    packageName);
         }
 
-        return new SyncNotedAppOp(result, code, isAttributionTagValid ? attributionTag : null,
-                resolvedPackageName);
+        synchronized (this) {
+            final Ops ops = getOpsLocked(uid, packageName, attributionTag,
+                    pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
+            if (ops == null) {
+                scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
+                        AppOpsManager.MODE_IGNORED);
+                if (DEBUG) Slog.d(TAG, "noteOperation: no op for code " + code + " uid " + uid
+                        + " package " + packageName + "flags: " +
+                        AppOpsManager.flagsToString(flags));
+                return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
+                        packageName);
+            }
+            final Op op = getOpLocked(ops, code, uid, true);
+            final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
+            if (attributedOp.isRunning()) {
+                Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + " code "
+                        + code + " startTime of in progress event="
+                        + attributedOp.mInProgressEvents.valueAt(0).getStartTime());
+            }
+
+            final int switchCode = AppOpsManager.opToSwitch(code);
+            final UidState uidState = ops.uidState;
+            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, false)) {
+                attributedOp.rejected(uidState.getState(), flags);
+                scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
+                        AppOpsManager.MODE_IGNORED);
+                return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
+                        packageName);
+            }
+            // If there is a non-default per UID policy (we set UID op mode only if
+            // non-default) it takes over, otherwise use the per package policy.
+            if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) {
+                final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode));
+                if (uidMode != AppOpsManager.MODE_ALLOWED) {
+                    if (DEBUG) Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code "
+                            + switchCode + " (" + code + ") uid " + uid + " package "
+                            + packageName + " flags: " + AppOpsManager.flagsToString(flags));
+                    attributedOp.rejected(uidState.getState(), flags);
+                    scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
+                            uidMode);
+                    return new SyncNotedAppOp(uidMode, code, attributionTag, packageName);
+                }
+            } else {
+                final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
+                        : op;
+                final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode());
+                if (mode != AppOpsManager.MODE_ALLOWED) {
+                    if (DEBUG) Slog.d(TAG, "noteOperation: reject #" + mode + " for code "
+                            + switchCode + " (" + code + ") uid " + uid + " package "
+                            + packageName + " flags: " + AppOpsManager.flagsToString(flags));
+                    attributedOp.rejected(uidState.getState(), flags);
+                    scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
+                            mode);
+                    return new SyncNotedAppOp(mode, code, attributionTag, packageName);
+                }
+            }
+            if (DEBUG) {
+                Slog.d(TAG,
+                        "noteOperation: allowing code " + code + " uid " + uid + " package "
+                                + packageName + (attributionTag == null ? ""
+                                : "." + attributionTag) + " flags: "
+                                + AppOpsManager.flagsToString(flags));
+            }
+            scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
+                    AppOpsManager.MODE_ALLOWED);
+            attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag,
+                    uidState.getState(),
+                    flags);
+
+            if (shouldCollectAsyncNotedOp) {
+                collectAsyncNotedOp(uid, packageName, code, attributionTag, flags, message,
+                        shouldCollectMessage);
+            }
+
+            return new SyncNotedAppOp(AppOpsManager.MODE_ALLOWED, code, attributionTag,
+                    packageName);
+        }
     }
 
     // TODO moltmann: Allow watching for attribution ops
     @Override
     public void startWatchingActive(int[] ops, IAppOpsActiveCallback callback) {
-        mAppOpsService.startWatchingActive(ops, callback);
+        int watchedUid = Process.INVALID_UID;
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
+                != PackageManager.PERMISSION_GRANTED) {
+            watchedUid = callingUid;
+        }
+        if (ops != null) {
+            Preconditions.checkArrayElementsInRange(ops, 0,
+                    AppOpsManager._NUM_OP - 1, "Invalid op code in: " + Arrays.toString(ops));
+        }
+        if (callback == null) {
+            return;
+        }
+        synchronized (this) {
+            SparseArray<ActiveCallback> callbacks = mActiveWatchers.get(callback.asBinder());
+            if (callbacks == null) {
+                callbacks = new SparseArray<>();
+                mActiveWatchers.put(callback.asBinder(), callbacks);
+            }
+            final ActiveCallback activeCallback = new ActiveCallback(callback, watchedUid,
+                    callingUid, callingPid);
+            for (int op : ops) {
+                callbacks.put(op, activeCallback);
+            }
+        }
     }
 
     @Override
     public void stopWatchingActive(IAppOpsActiveCallback callback) {
-        mAppOpsService.stopWatchingActive(callback);
+        if (callback == null) {
+            return;
+        }
+        synchronized (this) {
+            final SparseArray<ActiveCallback> activeCallbacks =
+                    mActiveWatchers.remove(callback.asBinder());
+            if (activeCallbacks == null) {
+                return;
+            }
+            final int callbackCount = activeCallbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                activeCallbacks.valueAt(i).destroy();
+            }
+        }
     }
 
     @Override
     public void startWatchingStarted(int[] ops, @NonNull IAppOpsStartedCallback callback) {
-        mAppOpsService.startWatchingStarted(ops, callback);
+        int watchedUid = Process.INVALID_UID;
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
+                != PackageManager.PERMISSION_GRANTED) {
+            watchedUid = callingUid;
+        }
+
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty");
+        Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1,
+                "Invalid op code in: " + Arrays.toString(ops));
+        Objects.requireNonNull(callback, "Callback cannot be null");
+
+        synchronized (this) {
+            SparseArray<StartedCallback> callbacks = mStartedWatchers.get(callback.asBinder());
+            if (callbacks == null) {
+                callbacks = new SparseArray<>();
+                mStartedWatchers.put(callback.asBinder(), callbacks);
+            }
+
+            final StartedCallback startedCallback = new StartedCallback(callback, watchedUid,
+                    callingUid, callingPid);
+            for (int op : ops) {
+                callbacks.put(op, startedCallback);
+            }
+        }
     }
 
     @Override
     public void stopWatchingStarted(IAppOpsStartedCallback callback) {
-        mAppOpsService.stopWatchingStarted(callback);
+        Objects.requireNonNull(callback, "Callback cannot be null");
+
+        synchronized (this) {
+            final SparseArray<StartedCallback> startedCallbacks =
+                    mStartedWatchers.remove(callback.asBinder());
+            if (startedCallbacks == null) {
+                return;
+            }
+
+            final int callbackCount = startedCallbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                startedCallbacks.valueAt(i).destroy();
+            }
+        }
     }
 
     @Override
     public void startWatchingNoted(@NonNull int[] ops, @NonNull IAppOpsNotedCallback callback) {
-        mAppOpsService.startWatchingNoted(ops, callback);
+        int watchedUid = Process.INVALID_UID;
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
+                != PackageManager.PERMISSION_GRANTED) {
+            watchedUid = callingUid;
+        }
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty");
+        Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1,
+                "Invalid op code in: " + Arrays.toString(ops));
+        Objects.requireNonNull(callback, "Callback cannot be null");
+        synchronized (this) {
+            SparseArray<NotedCallback> callbacks = mNotedWatchers.get(callback.asBinder());
+            if (callbacks == null) {
+                callbacks = new SparseArray<>();
+                mNotedWatchers.put(callback.asBinder(), callbacks);
+            }
+            final NotedCallback notedCallback = new NotedCallback(callback, watchedUid,
+                    callingUid, callingPid);
+            for (int op : ops) {
+                callbacks.put(op, notedCallback);
+            }
+        }
     }
 
     @Override
     public void stopWatchingNoted(IAppOpsNotedCallback callback) {
-        mAppOpsService.stopWatchingNoted(callback);
+        Objects.requireNonNull(callback, "Callback cannot be null");
+        synchronized (this) {
+            final SparseArray<NotedCallback> notedCallbacks =
+                    mNotedWatchers.remove(callback.asBinder());
+            if (notedCallbacks == null) {
+                return;
+            }
+            final int callbackCount = notedCallbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                notedCallbacks.valueAt(i).destroy();
+            }
+        }
     }
 
     /**
@@ -772,7 +2842,7 @@
         int uid = Binder.getCallingUid();
         Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid);
 
-        mAppOpsService.verifyPackage(uid, packageName);
+        verifyAndGetBypass(uid, packageName, null);
 
         synchronized (this) {
             RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key);
@@ -802,7 +2872,7 @@
         int uid = Binder.getCallingUid();
         Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid);
 
-        mAppOpsService.verifyPackage(uid, packageName);
+        verifyAndGetBypass(uid, packageName, null);
 
         synchronized (this) {
             RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key);
@@ -821,7 +2891,7 @@
 
         int uid = Binder.getCallingUid();
 
-        mAppOpsService.verifyPackage(uid, packageName);
+        verifyAndGetBypass(uid, packageName, null);
 
         synchronized (this) {
             return mUnforwardedAsyncNotedOps.remove(getAsyncNotedOpsKey(packageName, uid));
@@ -844,49 +2914,54 @@
             boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, @NonNull String message,
             boolean shouldCollectMessage, @AttributionFlags int attributionFlags,
             int attributionChainId) {
+        verifyIncomingUid(uid);
+        verifyIncomingOp(code);
         if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
                     packageName);
         }
 
-        int result = mAppOpsService.startOperation(clientId, code, uid, packageName,
-                attributionTag, startIfModeDefault, message,
-                attributionFlags, attributionChainId);
-
         String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
-
-        boolean isAttributionTagValid = mAppOpsService.isAttributionTagValid(uid,
-                resolvedPackageName, attributionTag, null);
-
-        if (shouldCollectAsyncNotedOp && result == MODE_ALLOWED) {
-            collectAsyncNotedOp(uid, resolvedPackageName, code,
-                    isAttributionTagValid ? attributionTag : null, AppOpsManager.OP_FLAG_SELF,
-                    message, shouldCollectMessage);
+        if (resolvedPackageName == null) {
+            return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag,
+                    packageName);
         }
 
-        return new SyncNotedAppOp(result, code, isAttributionTagValid ? attributionTag : null,
-                resolvedPackageName);
+        // As a special case for OP_RECORD_AUDIO_HOTWORD, which we use only for attribution
+        // purposes and not as a check, also make sure that the caller is allowed to access
+        // the data gated by OP_RECORD_AUDIO.
+        //
+        // TODO: Revert this change before Android 12.
+        if (code == OP_RECORD_AUDIO_HOTWORD || code == OP_RECEIVE_AMBIENT_TRIGGER_AUDIO) {
+            int result = checkOperation(OP_RECORD_AUDIO, uid, packageName);
+            if (result != AppOpsManager.MODE_ALLOWED) {
+                return new SyncNotedAppOp(result, code, attributionTag, packageName);
+            }
+        }
+        return startOperationUnchecked(clientId, code, uid, packageName, attributionTag,
+                Process.INVALID_UID, null, null, OP_FLAG_SELF, startIfModeDefault,
+                shouldCollectAsyncNotedOp, message, shouldCollectMessage, attributionFlags,
+                attributionChainId, /*dryRun*/ false);
     }
 
     @Override
-    public SyncNotedAppOp startProxyOperation(IBinder clientId, int code,
+    public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
             @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
             boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
             boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
             @AttributionFlags int proxiedAttributionFlags, int attributionChainId) {
-        return mCheckOpsDelegateDispatcher.startProxyOperation(clientId, code,
-                attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message,
-                shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
-                proxiedAttributionFlags, attributionChainId);
+        return mCheckOpsDelegateDispatcher.startProxyOperation(clientId, code, attributionSource,
+                startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
+                skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags,
+                attributionChainId);
     }
 
-    private SyncNotedAppOp startProxyOperationImpl(IBinder clientId, int code,
+    private SyncNotedAppOp startProxyOperationImpl(@NonNull IBinder clientId, int code,
             @NonNull AttributionSource attributionSource,
             boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message,
             boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags
             int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags,
             int attributionChainId) {
-
         final int proxyUid = attributionSource.getUid();
         final String proxyPackageName = attributionSource.getPackageName();
         final String proxyAttributionTag = attributionSource.getAttributionTag();
@@ -934,68 +3009,147 @@
 
         if (!skipProxyOperation) {
             // Test if the proxied operation will succeed before starting the proxy operation
-            final int testProxiedOp = mAppOpsService.startOperationUnchecked(clientId, code,
+            final SyncNotedAppOp testProxiedOp = startOperationUnchecked(clientId, code,
                     proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag, proxyUid,
                     resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault,
+                    shouldCollectAsyncNotedOp, message, shouldCollectMessage,
                     proxiedAttributionFlags, attributionChainId, /*dryRun*/ true);
-
-            boolean isTestProxiedAttributionTagValid =
-                    mAppOpsService.isAttributionTagValid(proxiedUid, resolvedProxiedPackageName,
-                            proxiedAttributionTag, resolvedProxyPackageName);
-
-            if (!shouldStartForMode(testProxiedOp, startIfModeDefault)) {
-                return new SyncNotedAppOp(testProxiedOp, code,
-                        isTestProxiedAttributionTagValid ? proxiedAttributionTag : null,
-                        resolvedProxiedPackageName);
+            if (!shouldStartForMode(testProxiedOp.getOpMode(), startIfModeDefault)) {
+                return testProxiedOp;
             }
 
             final int proxyFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXY
                     : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY;
 
-            final int proxyAppOp = mAppOpsService.startOperationUnchecked(clientId, code, proxyUid,
+            final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid,
                     resolvedProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null,
-                    proxyFlags, startIfModeDefault, proxyAttributionFlags, attributionChainId,
+                    proxyFlags, startIfModeDefault, !isProxyTrusted, "proxy " + message,
+                    shouldCollectMessage, proxyAttributionFlags, attributionChainId,
                     /*dryRun*/ false);
-
-            boolean isProxyAttributionTagValid = mAppOpsService.isAttributionTagValid(proxyUid,
-                    resolvedProxyPackageName, proxyAttributionTag, null);
-
-            if (!shouldStartForMode(proxyAppOp, startIfModeDefault)) {
-                return new SyncNotedAppOp(proxyAppOp, code,
-                        isProxyAttributionTagValid ? proxyAttributionTag : null,
-                        resolvedProxyPackageName);
-            }
-
-            if (shouldCollectAsyncNotedOp) {
-                collectAsyncNotedOp(proxyUid, resolvedProxyPackageName, code,
-                        isProxyAttributionTagValid ? proxyAttributionTag : null, proxyFlags,
-                        message, shouldCollectMessage);
+            if (!shouldStartForMode(proxyAppOp.getOpMode(), startIfModeDefault)) {
+                return proxyAppOp;
             }
         }
 
-        final int proxiedAppOp = mAppOpsService.startOperationUnchecked(clientId, code, proxiedUid,
-                resolvedProxiedPackageName, proxiedAttributionTag, proxyUid,
-                resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault,
-                proxiedAttributionFlags, attributionChainId,/*dryRun*/ false);
-
-        boolean isProxiedAttributionTagValid = mAppOpsService.isAttributionTagValid(proxiedUid,
-                resolvedProxiedPackageName, proxiedAttributionTag, resolvedProxyPackageName);
-
-        if (shouldCollectAsyncNotedOp && proxiedAppOp == MODE_ALLOWED) {
-            collectAsyncNotedOp(proxyUid, resolvedProxiedPackageName, code,
-                    isProxiedAttributionTagValid ? proxiedAttributionTag : null,
-                    proxiedAttributionFlags, message, shouldCollectMessage);
-        }
-
-        return new SyncNotedAppOp(proxiedAppOp, code,
-                isProxiedAttributionTagValid ? proxiedAttributionTag : null,
-                resolvedProxiedPackageName);
+        return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
+                proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag,
+                proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message,
+                shouldCollectMessage, proxiedAttributionFlags, attributionChainId,
+                /*dryRun*/ false);
     }
 
     private boolean shouldStartForMode(int mode, boolean startIfModeDefault) {
         return (mode == MODE_ALLOWED || (mode == MODE_DEFAULT && startIfModeDefault));
     }
 
+    private SyncNotedAppOp startOperationUnchecked(IBinder clientId, int code, int uid,
+            @NonNull String packageName, @Nullable String attributionTag, int proxyUid,
+            String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags,
+            boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, @Nullable String message,
+            boolean shouldCollectMessage, @AttributionFlags int attributionFlags,
+            int attributionChainId, boolean dryRun) {
+        PackageVerificationResult pvr;
+        try {
+            pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
+            if (!pvr.isAttributionTagValid) {
+                attributionTag = null;
+            }
+        } catch (SecurityException e) {
+            Slog.e(TAG, "startOperation", e);
+            return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
+                    packageName);
+        }
+
+        boolean isRestricted = false;
+        int startType = START_TYPE_FAILED;
+        synchronized (this) {
+            final Ops ops = getOpsLocked(uid, packageName, attributionTag,
+                    pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
+            if (ops == null) {
+                if (!dryRun) {
+                    scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
+                            flags, AppOpsManager.MODE_IGNORED, startType, attributionFlags,
+                            attributionChainId);
+                }
+                if (DEBUG) Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid
+                        + " package " + packageName + " flags: "
+                        + AppOpsManager.flagsToString(flags));
+                return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
+                        packageName);
+            }
+            final Op op = getOpLocked(ops, code, uid, true);
+            final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
+            final UidState uidState = ops.uidState;
+            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass,
+                    false);
+            final int switchCode = AppOpsManager.opToSwitch(code);
+            // If there is a non-default per UID policy (we set UID op mode only if
+            // non-default) it takes over, otherwise use the per package policy.
+            if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) {
+                final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode));
+                if (!shouldStartForMode(uidMode, startIfModeDefault)) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code "
+                                + switchCode + " (" + code + ") uid " + uid + " package "
+                                + packageName + " flags: " + AppOpsManager.flagsToString(flags));
+                    }
+                    if (!dryRun) {
+                        attributedOp.rejected(uidState.getState(), flags);
+                        scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
+                                flags, uidMode, startType, attributionFlags, attributionChainId);
+                    }
+                    return new SyncNotedAppOp(uidMode, code, attributionTag, packageName);
+                }
+            } else {
+                final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
+                        : op;
+                final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode());
+                if (mode != AppOpsManager.MODE_ALLOWED
+                        && (!startIfModeDefault || mode != MODE_DEFAULT)) {
+                    if (DEBUG) Slog.d(TAG, "startOperation: reject #" + mode + " for code "
+                            + switchCode + " (" + code + ") uid " + uid + " package "
+                            + packageName + " flags: " + AppOpsManager.flagsToString(flags));
+                    if (!dryRun) {
+                        attributedOp.rejected(uidState.getState(), flags);
+                        scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
+                                flags, mode, startType, attributionFlags, attributionChainId);
+                    }
+                    return new SyncNotedAppOp(mode, code, attributionTag, packageName);
+                }
+            }
+            if (DEBUG) Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid
+                    + " package " + packageName + " restricted: " + isRestricted
+                    + " flags: " + AppOpsManager.flagsToString(flags));
+            if (!dryRun) {
+                try {
+                    if (isRestricted) {
+                        attributedOp.createPaused(clientId, proxyUid, proxyPackageName,
+                                proxyAttributionTag, uidState.getState(), flags,
+                                attributionFlags, attributionChainId);
+                    } else {
+                        attributedOp.started(clientId, proxyUid, proxyPackageName,
+                                proxyAttributionTag, uidState.getState(), flags,
+                                attributionFlags, attributionChainId);
+                        startType = START_TYPE_STARTED;
+                    }
+                } catch (RemoteException e) {
+                    throw new RuntimeException(e);
+                }
+                scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, flags,
+                        isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags,
+                        attributionChainId);
+            }
+        }
+
+        if (shouldCollectAsyncNotedOp && !dryRun && !isRestricted) {
+            collectAsyncNotedOp(uid, packageName, code, attributionTag, AppOpsManager.OP_FLAG_SELF,
+                    message, shouldCollectMessage);
+        }
+
+        return new SyncNotedAppOp(isRestricted ? MODE_IGNORED : MODE_ALLOWED, code, attributionTag,
+                packageName);
+    }
+
     @Override
     public void finishOperation(IBinder clientId, int code, int uid, String packageName,
             String attributionTag) {
@@ -1005,11 +3159,22 @@
 
     private void finishOperationImpl(IBinder clientId, int code, int uid, String packageName,
             String attributionTag) {
-        mAppOpsService.finishOperation(clientId, code, uid, packageName, attributionTag);
+        verifyIncomingUid(uid);
+        verifyIncomingOp(code);
+        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
+            return;
+        }
+
+        String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return;
+        }
+
+        finishOperationUnchecked(clientId, code, uid, resolvedPackageName, attributionTag);
     }
 
     @Override
-    public void finishProxyOperation(IBinder clientId, int code,
+    public void finishProxyOperation(@NonNull IBinder clientId, int code,
             @NonNull AttributionSource attributionSource, boolean skipProxyOperation) {
         mCheckOpsDelegateDispatcher.finishProxyOperation(clientId, code, attributionSource,
                 skipProxyOperation);
@@ -1041,8 +3206,8 @@
         }
 
         if (!skipProxyOperation) {
-            mAppOpsService.finishOperationUnchecked(clientId, code, proxyUid,
-                    resolvedProxyPackageName, proxyAttributionTag);
+            finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName,
+                    proxyAttributionTag);
         }
 
         String resolvedProxiedPackageName = AppOpsManager.resolvePackageName(proxiedUid,
@@ -1051,12 +3216,209 @@
             return null;
         }
 
-        mAppOpsService.finishOperationUnchecked(clientId, code, proxiedUid,
-                resolvedProxiedPackageName, proxiedAttributionTag);
+        finishOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
+                proxiedAttributionTag);
 
         return null;
     }
 
+    private void finishOperationUnchecked(IBinder clientId, int code, int uid, String packageName,
+            String attributionTag) {
+        PackageVerificationResult pvr;
+        try {
+            pvr = verifyAndGetBypass(uid, packageName, attributionTag);
+            if (!pvr.isAttributionTagValid) {
+                attributionTag = null;
+            }
+        } catch (SecurityException e) {
+            Slog.e(TAG, "Cannot finishOperation", e);
+            return;
+        }
+
+        synchronized (this) {
+            Op op = getOpLocked(code, uid, packageName, attributionTag, pvr.isAttributionTagValid,
+                    pvr.bypass, /* edit */ true);
+            if (op == null) {
+                Slog.e(TAG, "Operation not found: uid=" + uid + " pkg=" + packageName + "("
+                        + attributionTag + ") op=" + AppOpsManager.opToName(code));
+                return;
+            }
+            final AttributedOp attributedOp = op.mAttributions.get(attributionTag);
+            if (attributedOp == null) {
+                Slog.e(TAG, "Attribution not found: uid=" + uid + " pkg=" + packageName + "("
+                        + attributionTag + ") op=" + AppOpsManager.opToName(code));
+                return;
+            }
+
+            if (attributedOp.isRunning() || attributedOp.isPaused()) {
+                attributedOp.finished(clientId);
+            } else {
+                Slog.e(TAG, "Operation not started: uid=" + uid + " pkg=" + packageName + "("
+                        + attributionTag + ") op=" + AppOpsManager.opToName(code));
+            }
+        }
+    }
+
+    void scheduleOpActiveChangedIfNeededLocked(int code, int uid, @NonNull
+            String packageName, @Nullable String attributionTag, boolean active, @AttributionFlags
+            int attributionFlags, int attributionChainId) {
+        ArraySet<ActiveCallback> dispatchedCallbacks = null;
+        final int callbackListCount = mActiveWatchers.size();
+        for (int i = 0; i < callbackListCount; i++) {
+            final SparseArray<ActiveCallback> callbacks = mActiveWatchers.valueAt(i);
+            ActiveCallback callback = callbacks.get(code);
+            if (callback != null) {
+                if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
+                    continue;
+                }
+                if (dispatchedCallbacks == null) {
+                    dispatchedCallbacks = new ArraySet<>();
+                }
+                dispatchedCallbacks.add(callback);
+            }
+        }
+        if (dispatchedCallbacks == null) {
+            return;
+        }
+        mHandler.sendMessage(PooledLambda.obtainMessage(
+                AppOpsService::notifyOpActiveChanged,
+                this, dispatchedCallbacks, code, uid, packageName, attributionTag, active,
+                attributionFlags, attributionChainId));
+    }
+
+    private void notifyOpActiveChanged(ArraySet<ActiveCallback> callbacks,
+            int code, int uid, @NonNull String packageName, @Nullable String attributionTag,
+            boolean active, @AttributionFlags int attributionFlags, int attributionChainId) {
+        // There are features watching for mode changes such as window manager
+        // and location manager which are in our process. The callbacks in these
+        // features may require permissions our remote caller does not have.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final int callbackCount = callbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                final ActiveCallback callback = callbacks.valueAt(i);
+                try {
+                    if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
+                        continue;
+                    }
+                    callback.mCallback.opActiveChanged(code, uid, packageName, attributionTag,
+                            active, attributionFlags, attributionChainId);
+                } catch (RemoteException e) {
+                    /* do nothing */
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName,
+            String attributionTag, @OpFlags int flags, @Mode int result,
+            @AppOpsManager.OnOpStartedListener.StartedType int startedType,
+            @AttributionFlags int attributionFlags, int attributionChainId) {
+        ArraySet<StartedCallback> dispatchedCallbacks = null;
+        final int callbackListCount = mStartedWatchers.size();
+        for (int i = 0; i < callbackListCount; i++) {
+            final SparseArray<StartedCallback> callbacks = mStartedWatchers.valueAt(i);
+
+            StartedCallback callback = callbacks.get(code);
+            if (callback != null) {
+                if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
+                    continue;
+                }
+
+                if (dispatchedCallbacks == null) {
+                    dispatchedCallbacks = new ArraySet<>();
+                }
+                dispatchedCallbacks.add(callback);
+            }
+        }
+
+        if (dispatchedCallbacks == null) {
+            return;
+        }
+
+        mHandler.sendMessage(PooledLambda.obtainMessage(
+                AppOpsService::notifyOpStarted,
+                this, dispatchedCallbacks, code, uid, pkgName, attributionTag, flags,
+                result, startedType, attributionFlags, attributionChainId));
+    }
+
+    private void notifyOpStarted(ArraySet<StartedCallback> callbacks,
+            int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
+            @Mode int result, @AppOpsManager.OnOpStartedListener.StartedType int startedType,
+            @AttributionFlags int attributionFlags, int attributionChainId) {
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final int callbackCount = callbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                final StartedCallback callback = callbacks.valueAt(i);
+                try {
+                    if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
+                        continue;
+                    }
+                    callback.mCallback.opStarted(code, uid, packageName, attributionTag, flags,
+                            result, startedType, attributionFlags, attributionChainId);
+                } catch (RemoteException e) {
+                    /* do nothing */
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private void scheduleOpNotedIfNeededLocked(int code, int uid, String packageName,
+            String attributionTag, @OpFlags int flags, @Mode int result) {
+        ArraySet<NotedCallback> dispatchedCallbacks = null;
+        final int callbackListCount = mNotedWatchers.size();
+        for (int i = 0; i < callbackListCount; i++) {
+            final SparseArray<NotedCallback> callbacks = mNotedWatchers.valueAt(i);
+            final NotedCallback callback = callbacks.get(code);
+            if (callback != null) {
+                if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
+                    continue;
+                }
+                if (dispatchedCallbacks == null) {
+                    dispatchedCallbacks = new ArraySet<>();
+                }
+                dispatchedCallbacks.add(callback);
+            }
+        }
+        if (dispatchedCallbacks == null) {
+            return;
+        }
+        mHandler.sendMessage(PooledLambda.obtainMessage(
+                AppOpsService::notifyOpChecked,
+                this, dispatchedCallbacks, code, uid, packageName, attributionTag, flags,
+                result));
+    }
+
+    private void notifyOpChecked(ArraySet<NotedCallback> callbacks,
+            int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
+            @Mode int result) {
+        // There are features watching for checks in our process. The callbacks in
+        // these features may require permissions our remote caller does not have.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final int callbackCount = callbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                final NotedCallback callback = callbacks.valueAt(i);
+                try {
+                    if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
+                        continue;
+                    }
+                    callback.mCallback.opNoted(code, uid, packageName, attributionTag, flags,
+                            result);
+                } catch (RemoteException e) {
+                    /* do nothing */
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
     @Override
     public int permissionToOpCode(String permission) {
         if (permission == null) {
@@ -1114,6 +3476,13 @@
                 Binder.getCallingPid(), Binder.getCallingUid(), null);
     }
 
+    private boolean shouldIgnoreCallback(int op, int watcherPid, int watcherUid) {
+        // If it's a restricted read op, ignore it if watcher doesn't have manage ops permission,
+        // as watcher should not use this to signal if the value is changed.
+        return opRestrictsRead(op) && mContext.checkPermission(Manifest.permission.MANAGE_APPOPS,
+                watcherPid, watcherUid) != PackageManager.PERMISSION_GRANTED;
+    }
+
     private void verifyIncomingOp(int op) {
         if (op >= 0 && op < AppOpsManager._NUM_OP) {
             // Enforce manage appops permission if it's a restricted read op.
@@ -1154,6 +3523,35 @@
                 || resolveUid(resolvedPackage) != Process.INVALID_UID;
     }
 
+    private boolean isCallerAndAttributionTrusted(@NonNull AttributionSource attributionSource) {
+        if (attributionSource.getUid() != Binder.getCallingUid()
+                && attributionSource.isTrusted(mContext)) {
+            return true;
+        }
+        return mContext.checkPermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
+                Binder.getCallingPid(), Binder.getCallingUid(), null)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    private @Nullable UidState getUidStateLocked(int uid, boolean edit) {
+        UidState uidState = mUidStates.get(uid);
+        if (uidState == null) {
+            if (!edit) {
+                return null;
+            }
+            uidState = new UidState(uid);
+            mUidStates.put(uid, uidState);
+        }
+
+        return uidState;
+    }
+
+    private void updateAppWidgetVisibility(SparseArray<String> uidPackageNames, boolean visible) {
+        synchronized (this) {
+            getUidStateTracker().updateAppWidgetVisibility(uidPackageNames, visible);
+        }
+    }
+
     /**
      * @return {@link PackageManagerInternal}
      */
@@ -1165,6 +3563,801 @@
         return mPackageManagerInternal;
     }
 
+    /**
+     * Create a restriction description matching the properties of the package.
+     *
+     * @param pkg The package to create the restriction description for
+     *
+     * @return The restriction matching the package
+     */
+    private RestrictionBypass getBypassforPackage(@NonNull AndroidPackage pkg) {
+        return new RestrictionBypass(pkg.getUid() == Process.SYSTEM_UID, pkg.isPrivileged(),
+                mContext.checkPermission(android.Manifest.permission
+                        .EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS, -1, pkg.getUid())
+                == PackageManager.PERMISSION_GRANTED);
+    }
+
+    /**
+     * @see #verifyAndGetBypass(int, String, String, String)
+     */
+    private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
+            @Nullable String attributionTag) {
+        return verifyAndGetBypass(uid, packageName, attributionTag, null);
+    }
+
+    /**
+     * Verify that package belongs to uid and return the {@link RestrictionBypass bypass
+     * description} for the package, along with a boolean indicating whether the attribution tag is
+     * valid.
+     *
+     * @param uid The uid the package belongs to
+     * @param packageName The package the might belong to the uid
+     * @param attributionTag attribution tag or {@code null} if no need to verify
+     * @param proxyPackageName The proxy package, from which the attribution tag is to be pulled
+     *
+     * @return PackageVerificationResult containing {@link RestrictionBypass} and whether the
+     *         attribution tag is valid
+     */
+    private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
+            @Nullable String attributionTag, @Nullable String proxyPackageName) {
+        if (uid == Process.ROOT_UID) {
+            // For backwards compatibility, don't check package name for root UID.
+            return new PackageVerificationResult(null,
+                    /* isAttributionTagValid */ true);
+        }
+        if (Process.isSdkSandboxUid(uid)) {
+            // SDK sandbox processes run in their own UID range, but their associated
+            // UID for checks should always be the UID of the package implementing SDK sandbox
+            // service.
+            // TODO: We will need to modify the callers of this function instead, so
+            // modifications and checks against the app ops state are done with the
+            // correct UID.
+            try {
+                final PackageManager pm = mContext.getPackageManager();
+                final String supplementalPackageName = pm.getSdkSandboxPackageName();
+                if (Objects.equals(packageName, supplementalPackageName)) {
+                    uid = pm.getPackageUidAsUser(supplementalPackageName,
+                            PackageManager.PackageInfoFlags.of(0), UserHandle.getUserId(uid));
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                // Shouldn't happen for the supplemental package
+                e.printStackTrace();
+            }
+        }
+
+
+        // Do not check if uid/packageName/attributionTag is already known.
+        synchronized (this) {
+            UidState uidState = mUidStates.get(uid);
+            if (uidState != null && uidState.pkgOps != null) {
+                Ops ops = uidState.pkgOps.get(packageName);
+
+                if (ops != null && (attributionTag == null || ops.knownAttributionTags.contains(
+                        attributionTag)) && ops.bypass != null) {
+                    return new PackageVerificationResult(ops.bypass,
+                            ops.validAttributionTags.contains(attributionTag));
+                }
+            }
+        }
+
+        int callingUid = Binder.getCallingUid();
+
+        // Allow any attribution tag for resolvable uids
+        int pkgUid;
+        if (Objects.equals(packageName, "com.android.shell")) {
+            // Special case for the shell which is a package but should be able
+            // to bypass app attribution tag restrictions.
+            pkgUid = Process.SHELL_UID;
+        } else {
+            pkgUid = resolveUid(packageName);
+        }
+        if (pkgUid != Process.INVALID_UID) {
+            if (pkgUid != UserHandle.getAppId(uid)) {
+                Slog.e(TAG, "Bad call made by uid " + callingUid + ". "
+                        + "Package \"" + packageName + "\" does not belong to uid " + uid + ".");
+                String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not";
+                throw new SecurityException("Specified package \"" + packageName + "\" under uid "
+                        +  UserHandle.getAppId(uid) + otherUidMessage);
+            }
+            return new PackageVerificationResult(RestrictionBypass.UNRESTRICTED,
+                    /* isAttributionTagValid */ true);
+        }
+
+        int userId = UserHandle.getUserId(uid);
+        RestrictionBypass bypass = null;
+        boolean isAttributionTagValid = false;
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class);
+            AndroidPackage pkg = pmInt.getPackage(packageName);
+            if (pkg != null) {
+                isAttributionTagValid = isAttributionInPackage(pkg, attributionTag);
+                pkgUid = UserHandle.getUid(userId, UserHandle.getAppId(pkg.getUid()));
+                bypass = getBypassforPackage(pkg);
+            }
+            if (!isAttributionTagValid) {
+                AndroidPackage proxyPkg = proxyPackageName != null
+                        ? pmInt.getPackage(proxyPackageName) : null;
+                // Re-check in proxy.
+                isAttributionTagValid = isAttributionInPackage(proxyPkg, attributionTag);
+                String msg;
+                if (pkg != null && isAttributionTagValid) {
+                    msg = "attributionTag " + attributionTag + " declared in manifest of the proxy"
+                            + " package " + proxyPackageName + ", this is not advised";
+                } else if (pkg != null) {
+                    msg = "attributionTag " + attributionTag + " not declared in manifest of "
+                            + packageName;
+                } else {
+                    msg = "package " + packageName + " not found, can't check for "
+                            + "attributionTag " + attributionTag;
+                }
+
+                try {
+                    if (!mPlatformCompat.isChangeEnabledByPackageName(
+                            SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE, packageName,
+                            userId) || !mPlatformCompat.isChangeEnabledByUid(
+                                    SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE,
+                            callingUid)) {
+                        // Do not override tags if overriding is not enabled for this package
+                        isAttributionTagValid = true;
+                    }
+                    Slog.e(TAG, msg);
+                } catch (RemoteException neverHappens) {
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+
+        if (pkgUid != uid) {
+            Slog.e(TAG, "Bad call made by uid " + callingUid + ". "
+                    + "Package \"" + packageName + "\" does not belong to uid " + uid + ".");
+            String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not";
+            throw new SecurityException("Specified package \"" + packageName + "\" under uid " + uid
+                    + otherUidMessage);
+        }
+
+        return new PackageVerificationResult(bypass, isAttributionTagValid);
+    }
+
+    private boolean isAttributionInPackage(@Nullable AndroidPackage pkg,
+            @Nullable String attributionTag) {
+        if (pkg == null) {
+            return false;
+        } else if (attributionTag == null) {
+            return true;
+        }
+        if (pkg.getAttributions() != null) {
+            int numAttributions = pkg.getAttributions().size();
+            for (int i = 0; i < numAttributions; i++) {
+                if (pkg.getAttributions().get(i).getTag().equals(attributionTag)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get (and potentially create) ops.
+     *
+     * @param uid The uid the package belongs to
+     * @param packageName The name of the package
+     * @param attributionTag attribution tag
+     * @param isAttributionTagValid whether the given attribution tag is valid
+     * @param bypass When to bypass certain op restrictions (can be null if edit == false)
+     * @param edit If an ops does not exist, create the ops?
+
+     * @return The ops
+     */
+    private Ops getOpsLocked(int uid, String packageName, @Nullable String attributionTag,
+            boolean isAttributionTagValid, @Nullable RestrictionBypass bypass, boolean edit) {
+        UidState uidState = getUidStateLocked(uid, edit);
+        if (uidState == null) {
+            return null;
+        }
+
+        if (uidState.pkgOps == null) {
+            if (!edit) {
+                return null;
+            }
+            uidState.pkgOps = new ArrayMap<>();
+        }
+
+        Ops ops = uidState.pkgOps.get(packageName);
+        if (ops == null) {
+            if (!edit) {
+                return null;
+            }
+            ops = new Ops(packageName, uidState);
+            uidState.pkgOps.put(packageName, ops);
+        }
+
+        if (edit) {
+            if (bypass != null) {
+                ops.bypass = bypass;
+            }
+
+            if (attributionTag != null) {
+                ops.knownAttributionTags.add(attributionTag);
+                if (isAttributionTagValid) {
+                    ops.validAttributionTags.add(attributionTag);
+                } else {
+                    ops.validAttributionTags.remove(attributionTag);
+                }
+            }
+        }
+
+        return ops;
+    }
+
+    @Override
+    public void scheduleWriteLocked() {
+        if (!mWriteScheduled) {
+            mWriteScheduled = true;
+            mHandler.postDelayed(mWriteRunner, WRITE_DELAY);
+        }
+    }
+
+    @Override
+    public void scheduleFastWriteLocked() {
+        if (!mFastWriteScheduled) {
+            mWriteScheduled = true;
+            mFastWriteScheduled = true;
+            mHandler.removeCallbacks(mWriteRunner);
+            mHandler.postDelayed(mWriteRunner, 10*1000);
+        }
+    }
+
+    /**
+     * Get the state of an op for a uid.
+     *
+     * @param code The code of the op
+     * @param uid The uid the of the package
+     * @param packageName The package name for which to get the state for
+     * @param attributionTag The attribution tag
+     * @param isAttributionTagValid Whether the given attribution tag is valid
+     * @param bypass When to bypass certain op restrictions (can be null if edit == false)
+     * @param edit Iff {@code true} create the {@link Op} object if not yet created
+     *
+     * @return The {@link Op state} of the op
+     */
+    private @Nullable Op getOpLocked(int code, int uid, @NonNull String packageName,
+            @Nullable String attributionTag, boolean isAttributionTagValid,
+            @Nullable RestrictionBypass bypass, boolean edit) {
+        Ops ops = getOpsLocked(uid, packageName, attributionTag, isAttributionTagValid, bypass,
+                edit);
+        if (ops == null) {
+            return null;
+        }
+        return getOpLocked(ops, code, uid, edit);
+    }
+
+    private Op getOpLocked(Ops ops, int code, int uid, boolean edit) {
+        Op op = ops.get(code);
+        if (op == null) {
+            if (!edit) {
+                return null;
+            }
+            op = new Op(ops.uidState, ops.packageName, code, uid);
+            ops.put(code, op);
+        }
+        if (edit) {
+            scheduleWriteLocked();
+        }
+        return op;
+    }
+
+    private boolean isOpRestrictedDueToSuspend(int code, String packageName, int uid) {
+        if (!ArrayUtils.contains(OPS_RESTRICTED_ON_SUSPEND, code)) {
+            return false;
+        }
+        final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+        return pmi.isPackageSuspended(packageName, UserHandle.getUserId(uid));
+    }
+
+    private boolean isOpRestrictedLocked(int uid, int code, String packageName,
+            String attributionTag, @Nullable RestrictionBypass appBypass, boolean isCheckOp) {
+        int restrictionSetCount = mOpGlobalRestrictions.size();
+
+        for (int i = 0; i < restrictionSetCount; i++) {
+            ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.valueAt(i);
+            if (restrictionState.hasRestriction(code)) {
+                return true;
+            }
+        }
+
+        int userHandle = UserHandle.getUserId(uid);
+        restrictionSetCount = mOpUserRestrictions.size();
+
+        for (int i = 0; i < restrictionSetCount; i++) {
+            // For each client, check that the given op is not restricted, or that the given
+            // package is exempt from the restriction.
+            ClientUserRestrictionState restrictionState = mOpUserRestrictions.valueAt(i);
+            if (restrictionState.hasRestriction(code, packageName, attributionTag, userHandle,
+                    isCheckOp)) {
+                RestrictionBypass opBypass = opAllowSystemBypassRestriction(code);
+                if (opBypass != null) {
+                    // If we are the system, bypass user restrictions for certain codes
+                    synchronized (this) {
+                        if (opBypass.isSystemUid && appBypass != null && appBypass.isSystemUid) {
+                            return false;
+                        }
+                        if (opBypass.isPrivileged && appBypass != null && appBypass.isPrivileged) {
+                            return false;
+                        }
+                        if (opBypass.isRecordAudioRestrictionExcept && appBypass != null
+                                && appBypass.isRecordAudioRestrictionExcept) {
+                            return false;
+                        }
+                    }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void readState() {
+        synchronized (mFile) {
+            synchronized (this) {
+                FileInputStream stream;
+                try {
+                    stream = mFile.openRead();
+                } catch (FileNotFoundException e) {
+                    Slog.i(TAG, "No existing app ops " + mFile.getBaseFile() + "; starting empty");
+                    return;
+                }
+                boolean success = false;
+                mUidStates.clear();
+                mAppOpsCheckingService.clearAllModes();
+                try {
+                    TypedXmlPullParser parser = Xml.resolvePullParser(stream);
+                    int type;
+                    while ((type = parser.next()) != XmlPullParser.START_TAG
+                            && type != XmlPullParser.END_DOCUMENT) {
+                        ;
+                    }
+
+                    if (type != XmlPullParser.START_TAG) {
+                        throw new IllegalStateException("no start tag found");
+                    }
+
+                    mVersionAtBoot = parser.getAttributeInt(null, "v", NO_VERSION);
+
+                    int outerDepth = parser.getDepth();
+                    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                            && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                            continue;
+                        }
+
+                        String tagName = parser.getName();
+                        if (tagName.equals("pkg")) {
+                            readPackage(parser);
+                        } else if (tagName.equals("uid")) {
+                            readUidOps(parser);
+                        } else {
+                            Slog.w(TAG, "Unknown element under <app-ops>: "
+                                    + parser.getName());
+                            XmlUtils.skipCurrentTag(parser);
+                        }
+                    }
+                    success = true;
+                } catch (IllegalStateException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (NullPointerException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (NumberFormatException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (XmlPullParserException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (IOException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (IndexOutOfBoundsException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } finally {
+                    if (!success) {
+                        mUidStates.clear();
+                        mAppOpsCheckingService.clearAllModes();
+                    }
+                    try {
+                        stream.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    @GuardedBy("this")
+    void upgradeRunAnyInBackgroundLocked() {
+        for (int i = 0; i < mUidStates.size(); i++) {
+            final UidState uidState = mUidStates.valueAt(i);
+            if (uidState == null) {
+                continue;
+            }
+            SparseIntArray opModes = uidState.getNonDefaultUidModes();
+            if (opModes != null) {
+                final int idx = opModes.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND);
+                if (idx >= 0) {
+                    uidState.setUidMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
+                            opModes.valueAt(idx));
+                }
+            }
+            if (uidState.pkgOps == null) {
+                continue;
+            }
+            boolean changed = false;
+            for (int j = 0; j < uidState.pkgOps.size(); j++) {
+                Ops ops = uidState.pkgOps.valueAt(j);
+                if (ops != null) {
+                    final Op op = ops.get(AppOpsManager.OP_RUN_IN_BACKGROUND);
+                    if (op != null && op.getMode() != AppOpsManager.opToDefaultMode(op.op)) {
+                        final Op copy = new Op(op.uidState, op.packageName,
+                                AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uidState.uid);
+                        copy.setMode(op.getMode());
+                        ops.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, copy);
+                        changed = true;
+                    }
+                }
+            }
+            if (changed) {
+                uidState.evalForegroundOps();
+            }
+        }
+    }
+
+    /**
+     * The interpretation of the default mode - MODE_DEFAULT - for OP_SCHEDULE_EXACT_ALARM is
+     * changing. Simultaneously, we want to change this op's mode from MODE_DEFAULT to MODE_ALLOWED
+     * for already installed apps. For newer apps, it will stay as MODE_DEFAULT.
+     */
+    @VisibleForTesting
+    @GuardedBy("this")
+    void upgradeScheduleExactAlarmLocked() {
+        final PermissionManagerServiceInternal pmsi = LocalServices.getService(
+                PermissionManagerServiceInternal.class);
+        final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+        final PackageManagerInternal pmi = getPackageManagerInternal();
+
+        final String[] packagesDeclaringPermission = pmsi.getAppOpPermissionPackages(
+                AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM));
+        final int[] userIds = umi.getUserIds();
+
+        for (final String pkg : packagesDeclaringPermission) {
+            for (int userId : userIds) {
+                final int uid = pmi.getPackageUid(pkg, 0, userId);
+
+                UidState uidState = mUidStates.get(uid);
+                if (uidState == null) {
+                    uidState = new UidState(uid);
+                    mUidStates.put(uid, uidState);
+                }
+                final int oldMode = uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM);
+                if (oldMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) {
+                    uidState.setUidMode(OP_SCHEDULE_EXACT_ALARM, MODE_ALLOWED);
+                }
+            }
+            // This appop is meant to be controlled at a uid level. So we leave package modes as
+            // they are.
+        }
+    }
+
+    private void upgradeLocked(int oldVersion) {
+        if (oldVersion >= CURRENT_VERSION) {
+            return;
+        }
+        Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION);
+        switch (oldVersion) {
+            case NO_VERSION:
+                upgradeRunAnyInBackgroundLocked();
+                // fall through
+            case 1:
+                upgradeScheduleExactAlarmLocked();
+                // fall through
+            case 2:
+                // for future upgrades
+        }
+        scheduleFastWriteLocked();
+    }
+
+    private void readUidOps(TypedXmlPullParser parser) throws NumberFormatException,
+            XmlPullParserException, IOException {
+        final int uid = parser.getAttributeInt(null, "n");
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals("op")) {
+                final int code = parser.getAttributeInt(null, "n");
+                final int mode = parser.getAttributeInt(null, "m");
+                setUidMode(code, uid, mode);
+            } else {
+                Slog.w(TAG, "Unknown element under <uid-ops>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+    }
+
+    private void readPackage(TypedXmlPullParser parser)
+            throws NumberFormatException, XmlPullParserException, IOException {
+        String pkgName = parser.getAttributeValue(null, "n");
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals("uid")) {
+                readUid(parser, pkgName);
+            } else {
+                Slog.w(TAG, "Unknown element under <pkg>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+    }
+
+    private void readUid(TypedXmlPullParser parser, String pkgName)
+            throws NumberFormatException, XmlPullParserException, IOException {
+        int uid = parser.getAttributeInt(null, "n");
+        final UidState uidState = getUidStateLocked(uid, true);
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+            String tagName = parser.getName();
+            if (tagName.equals("op")) {
+                readOp(parser, uidState, pkgName);
+            } else {
+                Slog.w(TAG, "Unknown element under <pkg>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        uidState.evalForegroundOps();
+    }
+
+    private void readAttributionOp(TypedXmlPullParser parser, @NonNull Op parent,
+            @Nullable String attribution)
+            throws NumberFormatException, IOException, XmlPullParserException {
+        final AttributedOp attributedOp = parent.getOrCreateAttribution(parent, attribution);
+
+        final long key = parser.getAttributeLong(null, "n");
+        final int uidState = extractUidStateFromKey(key);
+        final int opFlags = extractFlagsFromKey(key);
+
+        final long accessTime = parser.getAttributeLong(null, "t", 0);
+        final long rejectTime = parser.getAttributeLong(null, "r", 0);
+        final long accessDuration = parser.getAttributeLong(null, "d", -1);
+        final String proxyPkg = XmlUtils.readStringAttribute(parser, "pp");
+        final int proxyUid = parser.getAttributeInt(null, "pu", Process.INVALID_UID);
+        final String proxyAttributionTag = XmlUtils.readStringAttribute(parser, "pc");
+
+        if (accessTime > 0) {
+            attributedOp.accessed(accessTime, accessDuration, proxyUid, proxyPkg,
+                    proxyAttributionTag, uidState, opFlags);
+        }
+        if (rejectTime > 0) {
+            attributedOp.rejected(rejectTime, uidState, opFlags);
+        }
+    }
+
+    private void readOp(TypedXmlPullParser parser,
+            @NonNull UidState uidState, @NonNull String pkgName)
+            throws NumberFormatException, XmlPullParserException, IOException {
+        int opCode = parser.getAttributeInt(null, "n");
+        Op op = new Op(uidState, pkgName, opCode, uidState.uid);
+
+        final int mode = parser.getAttributeInt(null, "m", AppOpsManager.opToDefaultMode(op.op));
+        op.setMode(mode);
+
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+            String tagName = parser.getName();
+            if (tagName.equals("st")) {
+                readAttributionOp(parser, op, XmlUtils.readStringAttribute(parser, "id"));
+            } else {
+                Slog.w(TAG, "Unknown element under <op>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+
+        if (uidState.pkgOps == null) {
+            uidState.pkgOps = new ArrayMap<>();
+        }
+        Ops ops = uidState.pkgOps.get(pkgName);
+        if (ops == null) {
+            ops = new Ops(pkgName, uidState);
+            uidState.pkgOps.put(pkgName, ops);
+        }
+        ops.put(op.op, op);
+    }
+
+    void writeState() {
+        synchronized (mFile) {
+            FileOutputStream stream;
+            try {
+                stream = mFile.startWrite();
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to write state: " + e);
+                return;
+            }
+
+            List<AppOpsManager.PackageOps> allOps = getPackagesForOps(null);
+
+            try {
+                TypedXmlSerializer out = Xml.resolveSerializer(stream);
+                out.startDocument(null, true);
+                out.startTag(null, "app-ops");
+                out.attributeInt(null, "v", CURRENT_VERSION);
+
+                SparseArray<SparseIntArray> uidStatesClone;
+                synchronized (this) {
+                    uidStatesClone = new SparseArray<>(mUidStates.size());
+
+                    final int uidStateCount = mUidStates.size();
+                    for (int uidStateNum = 0; uidStateNum < uidStateCount; uidStateNum++) {
+                        UidState uidState = mUidStates.valueAt(uidStateNum);
+                        int uid = mUidStates.keyAt(uidStateNum);
+
+                        SparseIntArray opModes = uidState.getNonDefaultUidModes();
+                        if (opModes != null && opModes.size() > 0) {
+                            uidStatesClone.put(uid, opModes);
+                        }
+                    }
+                }
+
+                final int uidStateCount = uidStatesClone.size();
+                for (int uidStateNum = 0; uidStateNum < uidStateCount; uidStateNum++) {
+                    SparseIntArray opModes = uidStatesClone.valueAt(uidStateNum);
+                    if (opModes != null && opModes.size() > 0) {
+                        out.startTag(null, "uid");
+                        out.attributeInt(null, "n", uidStatesClone.keyAt(uidStateNum));
+                        final int opCount = opModes.size();
+                        for (int opCountNum = 0; opCountNum < opCount; opCountNum++) {
+                            final int op = opModes.keyAt(opCountNum);
+                            final int mode = opModes.valueAt(opCountNum);
+                            out.startTag(null, "op");
+                            out.attributeInt(null, "n", op);
+                            out.attributeInt(null, "m", mode);
+                            out.endTag(null, "op");
+                        }
+                        out.endTag(null, "uid");
+                    }
+                }
+
+                if (allOps != null) {
+                    String lastPkg = null;
+                    for (int i=0; i<allOps.size(); i++) {
+                        AppOpsManager.PackageOps pkg = allOps.get(i);
+                        if (!Objects.equals(pkg.getPackageName(), lastPkg)) {
+                            if (lastPkg != null) {
+                                out.endTag(null, "pkg");
+                            }
+                            lastPkg = pkg.getPackageName();
+                            if (lastPkg != null) {
+                                out.startTag(null, "pkg");
+                                out.attribute(null, "n", lastPkg);
+                            }
+                        }
+                        out.startTag(null, "uid");
+                        out.attributeInt(null, "n", pkg.getUid());
+                        List<AppOpsManager.OpEntry> ops = pkg.getOps();
+                        for (int j=0; j<ops.size(); j++) {
+                            AppOpsManager.OpEntry op = ops.get(j);
+                            out.startTag(null, "op");
+                            out.attributeInt(null, "n", op.getOp());
+                            if (op.getMode() != AppOpsManager.opToDefaultMode(op.getOp())) {
+                                out.attributeInt(null, "m", op.getMode());
+                            }
+
+                            for (String attributionTag : op.getAttributedOpEntries().keySet()) {
+                                final AttributedOpEntry attribution =
+                                        op.getAttributedOpEntries().get(attributionTag);
+
+                                final ArraySet<Long> keys = attribution.collectKeys();
+
+                                final int keyCount = keys.size();
+                                for (int k = 0; k < keyCount; k++) {
+                                    final long key = keys.valueAt(k);
+
+                                    final int uidState = AppOpsManager.extractUidStateFromKey(key);
+                                    final int flags = AppOpsManager.extractFlagsFromKey(key);
+
+                                    final long accessTime = attribution.getLastAccessTime(uidState,
+                                            uidState, flags);
+                                    final long rejectTime = attribution.getLastRejectTime(uidState,
+                                            uidState, flags);
+                                    final long accessDuration = attribution.getLastDuration(
+                                            uidState, uidState, flags);
+                                    // Proxy information for rejections is not backed up
+                                    final OpEventProxyInfo proxy = attribution.getLastProxyInfo(
+                                            uidState, uidState, flags);
+
+                                    if (accessTime <= 0 && rejectTime <= 0 && accessDuration <= 0
+                                            && proxy == null) {
+                                        continue;
+                                    }
+
+                                    String proxyPkg = null;
+                                    String proxyAttributionTag = null;
+                                    int proxyUid = Process.INVALID_UID;
+                                    if (proxy != null) {
+                                        proxyPkg = proxy.getPackageName();
+                                        proxyAttributionTag = proxy.getAttributionTag();
+                                        proxyUid = proxy.getUid();
+                                    }
+
+                                    out.startTag(null, "st");
+                                    if (attributionTag != null) {
+                                        out.attribute(null, "id", attributionTag);
+                                    }
+                                    out.attributeLong(null, "n", key);
+                                    if (accessTime > 0) {
+                                        out.attributeLong(null, "t", accessTime);
+                                    }
+                                    if (rejectTime > 0) {
+                                        out.attributeLong(null, "r", rejectTime);
+                                    }
+                                    if (accessDuration > 0) {
+                                        out.attributeLong(null, "d", accessDuration);
+                                    }
+                                    if (proxyPkg != null) {
+                                        out.attribute(null, "pp", proxyPkg);
+                                    }
+                                    if (proxyAttributionTag != null) {
+                                        out.attribute(null, "pc", proxyAttributionTag);
+                                    }
+                                    if (proxyUid >= 0) {
+                                        out.attributeInt(null, "pu", proxyUid);
+                                    }
+                                    out.endTag(null, "st");
+                                }
+                            }
+
+                            out.endTag(null, "op");
+                        }
+                        out.endTag(null, "uid");
+                    }
+                    if (lastPkg != null) {
+                        out.endTag(null, "pkg");
+                    }
+                }
+
+                out.endTag(null, "app-ops");
+                out.endDocument();
+                mFile.finishWrite(stream);
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to write state, restoring backup.", e);
+                mFile.failWrite(stream);
+            }
+        }
+        mHistoricalRegistry.writeAndClearDiscreteHistory();
+    }
+
     static class Shell extends ShellCommand {
         final IAppOpsService mInterface;
         final AppOpsService mInternal;
@@ -1178,6 +4371,7 @@
         int mode;
         int packageUid;
         int nonpackageUid;
+        final static Binder sBinder = new Binder();
         IBinder mToken;
         boolean targetsUid;
 
@@ -1198,7 +4392,7 @@
             dumpCommandHelp(pw);
         }
 
-        static int strOpToOp(String op, PrintWriter err) {
+        static private int strOpToOp(String op, PrintWriter err) {
             try {
                 return AppOpsManager.strOpToOp(op);
             } catch (IllegalArgumentException e) {
@@ -1395,24 +4589,6 @@
         pw.println("              not specified, the current user is assumed.");
     }
 
-    @Override
-    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        mAppOpsService.dump(fd, pw, args);
-
-        pw.println();
-        if (mCheckOpsDelegateDispatcher.mPolicy != null
-                && mCheckOpsDelegateDispatcher.mPolicy instanceof AppOpsPolicy) {
-            AppOpsPolicy policy = (AppOpsPolicy) mCheckOpsDelegateDispatcher.mPolicy;
-            policy.dumpTags(pw);
-        } else {
-            pw.println("  AppOps policy not set.");
-        }
-
-        if (mAudioRestrictionManager.hasActiveRestrictions()) {
-            pw.println();
-            mAudioRestrictionManager.dump(pw);
-        }
-    }
     static int onShellCommand(Shell shell, String cmd) {
         if (cmd == null) {
             return shell.handleDefaultCommands(cmd);
@@ -1616,12 +4792,14 @@
                     return 0;
                 }
                 case "write-settings": {
-                    shell.mInternal.mAppOpsService
-                            .enforceManageAppOpsModes(Binder.getCallingPid(),
+                    shell.mInternal.enforceManageAppOpsModes(Binder.getCallingPid(),
                             Binder.getCallingUid(), -1);
                     final long token = Binder.clearCallingIdentity();
                     try {
-                        shell.mInternal.mAppOpsService.writeState();
+                        synchronized (shell.mInternal) {
+                            shell.mInternal.mHandler.removeCallbacks(shell.mInternal.mWriteRunner);
+                        }
+                        shell.mInternal.writeState();
                         pw.println("Current settings written.");
                     } finally {
                         Binder.restoreCallingIdentity(token);
@@ -1629,12 +4807,11 @@
                     return 0;
                 }
                 case "read-settings": {
-                    shell.mInternal.mAppOpsService
-                            .enforceManageAppOpsModes(Binder.getCallingPid(),
-                                    Binder.getCallingUid(), -1);
+                    shell.mInternal.enforceManageAppOpsModes(Binder.getCallingPid(),
+                            Binder.getCallingUid(), -1);
                     final long token = Binder.clearCallingIdentity();
                     try {
-                        shell.mInternal.mAppOpsService.readState();
+                        shell.mInternal.readState();
                         pw.println("Last settings read.");
                     } finally {
                         Binder.restoreCallingIdentity(token);
@@ -1680,70 +4857,877 @@
         return -1;
     }
 
+    private void dumpHelp(PrintWriter pw) {
+        pw.println("AppOps service (appops) dump options:");
+        pw.println("  -h");
+        pw.println("    Print this help text.");
+        pw.println("  --op [OP]");
+        pw.println("    Limit output to data associated with the given app op code.");
+        pw.println("  --mode [MODE]");
+        pw.println("    Limit output to data associated with the given app op mode.");
+        pw.println("  --package [PACKAGE]");
+        pw.println("    Limit output to data associated with the given package name.");
+        pw.println("  --attributionTag [attributionTag]");
+        pw.println("    Limit output to data associated with the given attribution tag.");
+        pw.println("  --include-discrete [n]");
+        pw.println("    Include discrete ops limited to n per dimension. Use zero for no limit.");
+        pw.println("  --watchers");
+        pw.println("    Only output the watcher sections.");
+        pw.println("  --history");
+        pw.println("    Only output history.");
+        pw.println("  --uid-state-changes");
+        pw.println("    Include logs about uid state changes.");
+    }
+
+    private void dumpStatesLocked(@NonNull PrintWriter pw, @Nullable String filterAttributionTag,
+            @HistoricalOpsRequestFilter int filter, long nowElapsed, @NonNull Op op, long now,
+            @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix) {
+        final int numAttributions = op.mAttributions.size();
+        for (int i = 0; i < numAttributions; i++) {
+            if ((filter & FILTER_BY_ATTRIBUTION_TAG) != 0 && !Objects.equals(
+                    op.mAttributions.keyAt(i), filterAttributionTag)) {
+                continue;
+            }
+
+            pw.print(prefix + op.mAttributions.keyAt(i) + "=[\n");
+            dumpStatesLocked(pw, nowElapsed, op, op.mAttributions.keyAt(i), now, sdf, date,
+                    prefix + "  ");
+            pw.print(prefix + "]\n");
+        }
+    }
+
+    private void dumpStatesLocked(@NonNull PrintWriter pw, long nowElapsed, @NonNull Op op,
+            @Nullable String attributionTag, long now, @NonNull SimpleDateFormat sdf,
+            @NonNull Date date, @NonNull String prefix) {
+
+        final AttributedOpEntry entry = op.createSingleAttributionEntryLocked(
+                attributionTag).getAttributedOpEntries().get(attributionTag);
+
+        final ArraySet<Long> keys = entry.collectKeys();
+
+        final int keyCount = keys.size();
+        for (int k = 0; k < keyCount; k++) {
+            final long key = keys.valueAt(k);
+
+            final int uidState = AppOpsManager.extractUidStateFromKey(key);
+            final int flags = AppOpsManager.extractFlagsFromKey(key);
+
+            final long accessTime = entry.getLastAccessTime(uidState, uidState, flags);
+            final long rejectTime = entry.getLastRejectTime(uidState, uidState, flags);
+            final long accessDuration = entry.getLastDuration(uidState, uidState, flags);
+            final OpEventProxyInfo proxy = entry.getLastProxyInfo(uidState, uidState, flags);
+
+            String proxyPkg = null;
+            String proxyAttributionTag = null;
+            int proxyUid = Process.INVALID_UID;
+            if (proxy != null) {
+                proxyPkg = proxy.getPackageName();
+                proxyAttributionTag = proxy.getAttributionTag();
+                proxyUid = proxy.getUid();
+            }
+
+            if (accessTime > 0) {
+                pw.print(prefix);
+                pw.print("Access: ");
+                pw.print(AppOpsManager.keyToString(key));
+                pw.print(" ");
+                date.setTime(accessTime);
+                pw.print(sdf.format(date));
+                pw.print(" (");
+                TimeUtils.formatDuration(accessTime - now, pw);
+                pw.print(")");
+                if (accessDuration > 0) {
+                    pw.print(" duration=");
+                    TimeUtils.formatDuration(accessDuration, pw);
+                }
+                if (proxyUid >= 0) {
+                    pw.print(" proxy[");
+                    pw.print("uid=");
+                    pw.print(proxyUid);
+                    pw.print(", pkg=");
+                    pw.print(proxyPkg);
+                    pw.print(", attributionTag=");
+                    pw.print(proxyAttributionTag);
+                    pw.print("]");
+                }
+                pw.println();
+            }
+
+            if (rejectTime > 0) {
+                pw.print(prefix);
+                pw.print("Reject: ");
+                pw.print(AppOpsManager.keyToString(key));
+                date.setTime(rejectTime);
+                pw.print(sdf.format(date));
+                pw.print(" (");
+                TimeUtils.formatDuration(rejectTime - now, pw);
+                pw.print(")");
+                if (proxyUid >= 0) {
+                    pw.print(" proxy[");
+                    pw.print("uid=");
+                    pw.print(proxyUid);
+                    pw.print(", pkg=");
+                    pw.print(proxyPkg);
+                    pw.print(", attributionTag=");
+                    pw.print(proxyAttributionTag);
+                    pw.print("]");
+                }
+                pw.println();
+            }
+        }
+
+        final AttributedOp attributedOp = op.mAttributions.get(attributionTag);
+        if (attributedOp.isRunning()) {
+            long earliestElapsedTime = Long.MAX_VALUE;
+            long maxNumStarts = 0;
+            int numInProgressEvents = attributedOp.mInProgressEvents.size();
+            for (int i = 0; i < numInProgressEvents; i++) {
+                AttributedOp.InProgressStartOpEvent event =
+                        attributedOp.mInProgressEvents.valueAt(i);
+
+                earliestElapsedTime = Math.min(earliestElapsedTime, event.getStartElapsedTime());
+                maxNumStarts = Math.max(maxNumStarts, event.mNumUnfinishedStarts);
+            }
+
+            pw.print(prefix + "Running start at: ");
+            TimeUtils.formatDuration(nowElapsed - earliestElapsedTime, pw);
+            pw.println();
+
+            if (maxNumStarts > 1) {
+                pw.print(prefix + "startNesting=");
+                pw.println(maxNumStarts);
+            }
+        }
+    }
+
+    @NeverCompile // Avoid size overhead of debugging code.
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return;
+
+        int dumpOp = OP_NONE;
+        String dumpPackage = null;
+        String dumpAttributionTag = null;
+        int dumpUid = Process.INVALID_UID;
+        int dumpMode = -1;
+        boolean dumpWatchers = false;
+        // TODO ntmyren: Remove the dumpHistory and dumpFilter
+        boolean dumpHistory = false;
+        boolean includeDiscreteOps = false;
+        boolean dumpUidStateChangeLogs = false;
+        int nDiscreteOps = 10;
+        @HistoricalOpsRequestFilter int dumpFilter = 0;
+        boolean dumpAll = false;
+
+        if (args != null) {
+            for (int i = 0; i < args.length; i++) {
+                String arg = args[i];
+                if ("-h".equals(arg)) {
+                    dumpHelp(pw);
+                    return;
+                } else if ("-a".equals(arg)) {
+                    // dump all data
+                    dumpAll = true;
+                } else if ("--op".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("No argument for --op option");
+                        return;
+                    }
+                    dumpOp = Shell.strOpToOp(args[i], pw);
+                    dumpFilter |= FILTER_BY_OP_NAMES;
+                    if (dumpOp < 0) {
+                        return;
+                    }
+                } else if ("--package".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("No argument for --package option");
+                        return;
+                    }
+                    dumpPackage = args[i];
+                    dumpFilter |= FILTER_BY_PACKAGE_NAME;
+                    try {
+                        dumpUid = AppGlobals.getPackageManager().getPackageUid(dumpPackage,
+                                PackageManager.MATCH_KNOWN_PACKAGES | PackageManager.MATCH_INSTANT,
+                                0);
+                    } catch (RemoteException e) {
+                    }
+                    if (dumpUid < 0) {
+                        pw.println("Unknown package: " + dumpPackage);
+                        return;
+                    }
+                    dumpUid = UserHandle.getAppId(dumpUid);
+                    dumpFilter |= FILTER_BY_UID;
+                } else if ("--attributionTag".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("No argument for --attributionTag option");
+                        return;
+                    }
+                    dumpAttributionTag = args[i];
+                    dumpFilter |= FILTER_BY_ATTRIBUTION_TAG;
+                } else if ("--mode".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("No argument for --mode option");
+                        return;
+                    }
+                    dumpMode = Shell.strModeToMode(args[i], pw);
+                    if (dumpMode < 0) {
+                        return;
+                    }
+                } else if ("--watchers".equals(arg)) {
+                    dumpWatchers = true;
+                } else if ("--include-discrete".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("No argument for --include-discrete option");
+                        return;
+                    }
+                    try {
+                        nDiscreteOps = Integer.valueOf(args[i]);
+                    } catch (NumberFormatException e) {
+                        pw.println("Wrong parameter: " + args[i]);
+                        return;
+                    }
+                    includeDiscreteOps = true;
+                } else if ("--history".equals(arg)) {
+                    dumpHistory = true;
+                } else if (arg.length() > 0 && arg.charAt(0) == '-') {
+                    pw.println("Unknown option: " + arg);
+                    return;
+                } else if ("--uid-state-changes".equals(arg)) {
+                    dumpUidStateChangeLogs = true;
+                } else {
+                    pw.println("Unknown command: " + arg);
+                    return;
+                }
+            }
+        }
+
+        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        final Date date = new Date();
+        synchronized (this) {
+            pw.println("Current AppOps Service state:");
+            if (!dumpHistory && !dumpWatchers) {
+                mConstants.dump(pw);
+            }
+            pw.println();
+            final long now = System.currentTimeMillis();
+            final long nowElapsed = SystemClock.elapsedRealtime();
+            final long nowUptime = SystemClock.uptimeMillis();
+            boolean needSep = false;
+            if (dumpFilter == 0 && dumpMode < 0 && mProfileOwners != null && !dumpWatchers
+                    && !dumpHistory) {
+                pw.println("  Profile owners:");
+                for (int poi = 0; poi < mProfileOwners.size(); poi++) {
+                    pw.print("    User #");
+                    pw.print(mProfileOwners.keyAt(poi));
+                    pw.print(": ");
+                    UserHandle.formatUid(pw, mProfileOwners.valueAt(poi));
+                    pw.println();
+                }
+                pw.println();
+            }
+
+            if (!dumpHistory) {
+                needSep |= mAppOpsCheckingService.dumpListeners(dumpOp, dumpUid, dumpPackage, pw);
+            }
+
+            if (mModeWatchers.size() > 0 && dumpOp < 0 && !dumpHistory) {
+                boolean printedHeader = false;
+                for (int i = 0; i < mModeWatchers.size(); i++) {
+                    final ModeCallback cb = mModeWatchers.valueAt(i);
+                    if (dumpPackage != null
+                            && dumpUid != UserHandle.getAppId(cb.getWatchingUid())) {
+                        continue;
+                    }
+                    needSep = true;
+                    if (!printedHeader) {
+                        pw.println("  All op mode watchers:");
+                        printedHeader = true;
+                    }
+                    pw.print("    ");
+                    pw.print(Integer.toHexString(System.identityHashCode(mModeWatchers.keyAt(i))));
+                    pw.print(": "); pw.println(cb);
+                }
+            }
+            if (mActiveWatchers.size() > 0 && dumpMode < 0) {
+                needSep = true;
+                boolean printedHeader = false;
+                for (int watcherNum = 0; watcherNum < mActiveWatchers.size(); watcherNum++) {
+                    final SparseArray<ActiveCallback> activeWatchers =
+                            mActiveWatchers.valueAt(watcherNum);
+                    if (activeWatchers.size() <= 0) {
+                        continue;
+                    }
+                    final ActiveCallback cb = activeWatchers.valueAt(0);
+                    if (dumpOp >= 0 && activeWatchers.indexOfKey(dumpOp) < 0) {
+                        continue;
+                    }
+                    if (dumpPackage != null
+                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
+                        continue;
+                    }
+                    if (!printedHeader) {
+                        pw.println("  All op active watchers:");
+                        printedHeader = true;
+                    }
+                    pw.print("    ");
+                    pw.print(Integer.toHexString(System.identityHashCode(
+                            mActiveWatchers.keyAt(watcherNum))));
+                    pw.println(" ->");
+                    pw.print("        [");
+                    final int opCount = activeWatchers.size();
+                    for (int opNum = 0; opNum < opCount; opNum++) {
+                        if (opNum > 0) {
+                            pw.print(' ');
+                        }
+                        pw.print(AppOpsManager.opToName(activeWatchers.keyAt(opNum)));
+                        if (opNum < opCount - 1) {
+                            pw.print(',');
+                        }
+                    }
+                    pw.println("]");
+                    pw.print("        ");
+                    pw.println(cb);
+                }
+            }
+            if (mStartedWatchers.size() > 0 && dumpMode < 0) {
+                needSep = true;
+                boolean printedHeader = false;
+
+                final int watchersSize = mStartedWatchers.size();
+                for (int watcherNum = 0; watcherNum < watchersSize; watcherNum++) {
+                    final SparseArray<StartedCallback> startedWatchers =
+                            mStartedWatchers.valueAt(watcherNum);
+                    if (startedWatchers.size() <= 0) {
+                        continue;
+                    }
+
+                    final StartedCallback cb = startedWatchers.valueAt(0);
+                    if (dumpOp >= 0 && startedWatchers.indexOfKey(dumpOp) < 0) {
+                        continue;
+                    }
+
+                    if (dumpPackage != null
+                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
+                        continue;
+                    }
+
+                    if (!printedHeader) {
+                        pw.println("  All op started watchers:");
+                        printedHeader = true;
+                    }
+
+                    pw.print("    ");
+                    pw.print(Integer.toHexString(System.identityHashCode(
+                            mStartedWatchers.keyAt(watcherNum))));
+                    pw.println(" ->");
+
+                    pw.print("        [");
+                    final int opCount = startedWatchers.size();
+                    for (int opNum = 0; opNum < opCount; opNum++) {
+                        if (opNum > 0) {
+                            pw.print(' ');
+                        }
+
+                        pw.print(AppOpsManager.opToName(startedWatchers.keyAt(opNum)));
+                        if (opNum < opCount - 1) {
+                            pw.print(',');
+                        }
+                    }
+                    pw.println("]");
+
+                    pw.print("        ");
+                    pw.println(cb);
+                }
+            }
+            if (mNotedWatchers.size() > 0 && dumpMode < 0) {
+                needSep = true;
+                boolean printedHeader = false;
+                for (int watcherNum = 0; watcherNum < mNotedWatchers.size(); watcherNum++) {
+                    final SparseArray<NotedCallback> notedWatchers =
+                            mNotedWatchers.valueAt(watcherNum);
+                    if (notedWatchers.size() <= 0) {
+                        continue;
+                    }
+                    final NotedCallback cb = notedWatchers.valueAt(0);
+                    if (dumpOp >= 0 && notedWatchers.indexOfKey(dumpOp) < 0) {
+                        continue;
+                    }
+                    if (dumpPackage != null
+                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
+                        continue;
+                    }
+                    if (!printedHeader) {
+                        pw.println("  All op noted watchers:");
+                        printedHeader = true;
+                    }
+                    pw.print("    ");
+                    pw.print(Integer.toHexString(System.identityHashCode(
+                            mNotedWatchers.keyAt(watcherNum))));
+                    pw.println(" ->");
+                    pw.print("        [");
+                    final int opCount = notedWatchers.size();
+                    for (int opNum = 0; opNum < opCount; opNum++) {
+                        if (opNum > 0) {
+                            pw.print(' ');
+                        }
+                        pw.print(AppOpsManager.opToName(notedWatchers.keyAt(opNum)));
+                        if (opNum < opCount - 1) {
+                            pw.print(',');
+                        }
+                    }
+                    pw.println("]");
+                    pw.print("        ");
+                    pw.println(cb);
+                }
+            }
+            if (mAudioRestrictionManager.hasActiveRestrictions() && dumpOp < 0
+                    && dumpPackage != null && dumpMode < 0 && !dumpWatchers) {
+                needSep = mAudioRestrictionManager.dump(pw) || needSep;
+            }
+            if (needSep) {
+                pw.println();
+            }
+            for (int i=0; i<mUidStates.size(); i++) {
+                UidState uidState = mUidStates.valueAt(i);
+                final SparseIntArray opModes = uidState.getNonDefaultUidModes();
+                final ArrayMap<String, Ops> pkgOps = uidState.pkgOps;
+
+                if (dumpWatchers || dumpHistory) {
+                    continue;
+                }
+                if (dumpOp >= 0 || dumpPackage != null || dumpMode >= 0) {
+                    boolean hasOp = dumpOp < 0 || (opModes != null
+                            && opModes.indexOfKey(dumpOp) >= 0);
+                    boolean hasPackage = dumpPackage == null || dumpUid == mUidStates.keyAt(i);
+                    boolean hasMode = dumpMode < 0;
+                    if (!hasMode && opModes != null) {
+                        for (int opi = 0; !hasMode && opi < opModes.size(); opi++) {
+                            if (opModes.valueAt(opi) == dumpMode) {
+                                hasMode = true;
+                            }
+                        }
+                    }
+                    if (pkgOps != null) {
+                        for (int pkgi = 0;
+                                 (!hasOp || !hasPackage || !hasMode) && pkgi < pkgOps.size();
+                                 pkgi++) {
+                            Ops ops = pkgOps.valueAt(pkgi);
+                            if (!hasOp && ops != null && ops.indexOfKey(dumpOp) >= 0) {
+                                hasOp = true;
+                            }
+                            if (!hasMode) {
+                                for (int opi = 0; !hasMode && opi < ops.size(); opi++) {
+                                    if (ops.valueAt(opi).getMode() == dumpMode) {
+                                        hasMode = true;
+                                    }
+                                }
+                            }
+                            if (!hasPackage && dumpPackage.equals(ops.packageName)) {
+                                hasPackage = true;
+                            }
+                        }
+                    }
+                    if (uidState.foregroundOps != null && !hasOp) {
+                        if (uidState.foregroundOps.indexOfKey(dumpOp) > 0) {
+                            hasOp = true;
+                        }
+                    }
+                    if (!hasOp || !hasPackage || !hasMode) {
+                        continue;
+                    }
+                }
+
+                pw.print("  Uid "); UserHandle.formatUid(pw, uidState.uid); pw.println(":");
+                uidState.dump(pw, nowElapsed);
+                if (uidState.foregroundOps != null && (dumpMode < 0
+                        || dumpMode == AppOpsManager.MODE_FOREGROUND)) {
+                    pw.println("    foregroundOps:");
+                    for (int j = 0; j < uidState.foregroundOps.size(); j++) {
+                        if (dumpOp >= 0 && dumpOp != uidState.foregroundOps.keyAt(j)) {
+                            continue;
+                        }
+                        pw.print("      ");
+                        pw.print(AppOpsManager.opToName(uidState.foregroundOps.keyAt(j)));
+                        pw.print(": ");
+                        pw.println(uidState.foregroundOps.valueAt(j) ? "WATCHER" : "SILENT");
+                    }
+                    pw.print("    hasForegroundWatchers=");
+                    pw.println(uidState.hasForegroundWatchers);
+                }
+                needSep = true;
+
+                if (opModes != null) {
+                    final int opModeCount = opModes.size();
+                    for (int j = 0; j < opModeCount; j++) {
+                        final int code = opModes.keyAt(j);
+                        final int mode = opModes.valueAt(j);
+                        if (dumpOp >= 0 && dumpOp != code) {
+                            continue;
+                        }
+                        if (dumpMode >= 0 && dumpMode != mode) {
+                            continue;
+                        }
+                        pw.print("      "); pw.print(AppOpsManager.opToName(code));
+                        pw.print(": mode="); pw.println(AppOpsManager.modeToName(mode));
+                    }
+                }
+
+                if (pkgOps == null) {
+                    continue;
+                }
+
+                for (int pkgi = 0; pkgi < pkgOps.size(); pkgi++) {
+                    final Ops ops = pkgOps.valueAt(pkgi);
+                    if (dumpPackage != null && !dumpPackage.equals(ops.packageName)) {
+                        continue;
+                    }
+                    boolean printedPackage = false;
+                    for (int j=0; j<ops.size(); j++) {
+                        final Op op = ops.valueAt(j);
+                        final int opCode = op.op;
+                        if (dumpOp >= 0 && dumpOp != opCode) {
+                            continue;
+                        }
+                        if (dumpMode >= 0 && dumpMode != op.getMode()) {
+                            continue;
+                        }
+                        if (!printedPackage) {
+                            pw.print("    Package "); pw.print(ops.packageName); pw.println(":");
+                            printedPackage = true;
+                        }
+                        pw.print("      "); pw.print(AppOpsManager.opToName(opCode));
+                        pw.print(" ("); pw.print(AppOpsManager.modeToName(op.getMode()));
+                        final int switchOp = AppOpsManager.opToSwitch(opCode);
+                        if (switchOp != opCode) {
+                            pw.print(" / switch ");
+                            pw.print(AppOpsManager.opToName(switchOp));
+                            final Op switchObj = ops.get(switchOp);
+                            int mode = switchObj == null
+                                    ? AppOpsManager.opToDefaultMode(switchOp) : switchObj.getMode();
+                            pw.print("="); pw.print(AppOpsManager.modeToName(mode));
+                        }
+                        pw.println("): ");
+                        dumpStatesLocked(pw, dumpAttributionTag, dumpFilter, nowElapsed, op, now,
+                                sdf, date, "        ");
+                    }
+                }
+            }
+            if (needSep) {
+                pw.println();
+            }
+
+            boolean showUserRestrictions = !(dumpMode < 0 && !dumpWatchers && !dumpHistory);
+            mAppOpsRestrictions.dumpRestrictions(pw, dumpOp, dumpPackage, showUserRestrictions);
+
+            if (!dumpHistory && !dumpWatchers) {
+                pw.println();
+                if (mCheckOpsDelegateDispatcher.mPolicy != null
+                        && mCheckOpsDelegateDispatcher.mPolicy instanceof AppOpsPolicy) {
+                    AppOpsPolicy policy = (AppOpsPolicy) mCheckOpsDelegateDispatcher.mPolicy;
+                    policy.dumpTags(pw);
+                } else {
+                    pw.println("  AppOps policy not set.");
+                }
+            }
+
+            if (dumpAll || dumpUidStateChangeLogs) {
+                pw.println();
+                pw.println("Uid State Changes Event Log:");
+                getUidStateTracker().dumpEvents(pw);
+            }
+        }
+
+        // Must not hold the appops lock
+        if (dumpHistory && !dumpWatchers) {
+            mHistoricalRegistry.dump("  ", pw, dumpUid, dumpPackage, dumpAttributionTag, dumpOp,
+                    dumpFilter);
+        }
+        if (includeDiscreteOps) {
+            pw.println("Discrete accesses: ");
+            mHistoricalRegistry.dumpDiscreteData(pw, dumpUid, dumpPackage, dumpAttributionTag,
+                    dumpFilter, dumpOp, sdf, date, "  ", nDiscreteOps);
+        }
+    }
+
     @Override
     public void setUserRestrictions(Bundle restrictions, IBinder token, int userHandle) {
-        mAppOpsService.setUserRestrictions(restrictions, token, userHandle);
+        checkSystemUid("setUserRestrictions");
+        Objects.requireNonNull(restrictions);
+        Objects.requireNonNull(token);
+        for (int i = 0; i < AppOpsManager._NUM_OP; i++) {
+            String restriction = AppOpsManager.opToRestriction(i);
+            if (restriction != null) {
+                setUserRestrictionNoCheck(i, restrictions.getBoolean(restriction, false), token,
+                        userHandle, null);
+            }
+        }
     }
 
     @Override
     public void setUserRestriction(int code, boolean restricted, IBinder token, int userHandle,
             PackageTagsList excludedPackageTags) {
-        mAppOpsService.setUserRestriction(code, restricted, token, userHandle,
-                excludedPackageTags);
+        if (Binder.getCallingPid() != Process.myPid()) {
+            mContext.enforcePermission(Manifest.permission.MANAGE_APP_OPS_RESTRICTIONS,
+                    Binder.getCallingPid(), Binder.getCallingUid(), null);
+        }
+        if (userHandle != UserHandle.getCallingUserId()) {
+            if (mContext.checkCallingOrSelfPermission(Manifest.permission
+                    .INTERACT_ACROSS_USERS_FULL) != PackageManager.PERMISSION_GRANTED
+                && mContext.checkCallingOrSelfPermission(Manifest.permission
+                    .INTERACT_ACROSS_USERS) != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Need INTERACT_ACROSS_USERS_FULL or"
+                        + " INTERACT_ACROSS_USERS to interact cross user ");
+            }
+        }
+        verifyIncomingOp(code);
+        Objects.requireNonNull(token);
+        setUserRestrictionNoCheck(code, restricted, token, userHandle, excludedPackageTags);
+    }
+
+    private void setUserRestrictionNoCheck(int code, boolean restricted, IBinder token,
+            int userHandle, PackageTagsList excludedPackageTags) {
+        synchronized (AppOpsService.this) {
+            ClientUserRestrictionState restrictionState = mOpUserRestrictions.get(token);
+
+            if (restrictionState == null) {
+                try {
+                    restrictionState = new ClientUserRestrictionState(token);
+                } catch (RemoteException e) {
+                    return;
+                }
+                mOpUserRestrictions.put(token, restrictionState);
+            }
+
+            if (restrictionState.setRestriction(code, restricted, excludedPackageTags,
+                    userHandle)) {
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        AppOpsService::notifyWatchersOfChange, this, code, UID_ANY));
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        AppOpsService::updateStartedOpModeForUser, this, code, restricted,
+                        userHandle));
+            }
+
+            if (restrictionState.isDefault()) {
+                mOpUserRestrictions.remove(token);
+                restrictionState.destroy();
+            }
+        }
+    }
+
+    private void updateStartedOpModeForUser(int code, boolean restricted, int userId) {
+        synchronized (AppOpsService.this) {
+            int numUids = mUidStates.size();
+            for (int uidNum = 0; uidNum < numUids; uidNum++) {
+                int uid = mUidStates.keyAt(uidNum);
+                if (userId != UserHandle.USER_ALL && UserHandle.getUserId(uid) != userId) {
+                    continue;
+                }
+                updateStartedOpModeForUidLocked(code, restricted, uid);
+            }
+        }
+    }
+
+    private void updateStartedOpModeForUidLocked(int code, boolean restricted, int uid) {
+        UidState uidState = mUidStates.get(uid);
+        if (uidState == null || uidState.pkgOps == null) {
+            return;
+        }
+
+        int numPkgOps = uidState.pkgOps.size();
+        for (int pkgNum = 0; pkgNum < numPkgOps; pkgNum++) {
+            Ops ops = uidState.pkgOps.valueAt(pkgNum);
+            Op op = ops != null ? ops.get(code) : null;
+            if (op == null || (op.getMode() != MODE_ALLOWED && op.getMode() != MODE_FOREGROUND)) {
+                continue;
+            }
+            int numAttrTags = op.mAttributions.size();
+            for (int attrNum = 0; attrNum < numAttrTags; attrNum++) {
+                AttributedOp attrOp = op.mAttributions.valueAt(attrNum);
+                if (restricted && attrOp.isRunning()) {
+                    attrOp.pause();
+                } else if (attrOp.isPaused()) {
+                    attrOp.resume();
+                }
+            }
+        }
+    }
+
+    private void notifyWatchersOfChange(int code, int uid) {
+        final ArraySet<OnOpModeChangedListener> modeChangedListenerSet;
+        synchronized (this) {
+            modeChangedListenerSet = mAppOpsCheckingService.getOpModeChangedListeners(code);
+            if (modeChangedListenerSet == null) {
+                return;
+            }
+        }
+
+        notifyOpChanged(modeChangedListenerSet,  code, uid, null);
     }
 
     @Override
     public void removeUser(int userHandle) throws RemoteException {
-        mAppOpsService.removeUser(userHandle);
+        checkSystemUid("removeUser");
+        synchronized (AppOpsService.this) {
+            final int tokenCount = mOpUserRestrictions.size();
+            for (int i = tokenCount - 1; i >= 0; i--) {
+                ClientUserRestrictionState opRestrictions = mOpUserRestrictions.valueAt(i);
+                opRestrictions.removeUser(userHandle);
+            }
+            removeUidsForUserLocked(userHandle);
+        }
     }
 
     @Override
     public boolean isOperationActive(int code, int uid, String packageName) {
-        return mAppOpsService.isOperationActive(code, uid, packageName);
+        if (Binder.getCallingUid() != uid) {
+            if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        verifyIncomingOp(code);
+        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
+            return false;
+        }
+
+        final String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return false;
+        }
+        // TODO moltmann: Allow to check for attribution op activeness
+        synchronized (AppOpsService.this) {
+            Ops pkgOps = getOpsLocked(uid, resolvedPackageName, null, false, null, false);
+            if (pkgOps == null) {
+                return false;
+            }
+
+            Op op = pkgOps.get(code);
+            if (op == null) {
+                return false;
+            }
+
+            return op.isRunning();
+        }
     }
 
     @Override
     public boolean isProxying(int op, @NonNull String proxyPackageName,
             @NonNull String proxyAttributionTag, int proxiedUid,
             @NonNull String proxiedPackageName) {
-        return mAppOpsService.isProxying(op, proxyPackageName, proxyAttributionTag,
-                proxiedUid, proxiedPackageName);
+        Objects.requireNonNull(proxyPackageName);
+        Objects.requireNonNull(proxiedPackageName);
+        final long callingUid = Binder.getCallingUid();
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final List<AppOpsManager.PackageOps> packageOps = getOpsForPackage(proxiedUid,
+                    proxiedPackageName, new int[] {op});
+            if (packageOps == null || packageOps.isEmpty()) {
+                return false;
+            }
+            final List<OpEntry> opEntries = packageOps.get(0).getOps();
+            if (opEntries.isEmpty()) {
+                return false;
+            }
+            final OpEntry opEntry = opEntries.get(0);
+            if (!opEntry.isRunning()) {
+                return false;
+            }
+            final OpEventProxyInfo proxyInfo = opEntry.getLastProxyInfo(
+                    OP_FLAG_TRUSTED_PROXIED | AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED);
+            return proxyInfo != null && callingUid == proxyInfo.getUid()
+                    && proxyPackageName.equals(proxyInfo.getPackageName())
+                    && Objects.equals(proxyAttributionTag, proxyInfo.getAttributionTag());
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
     }
 
     @Override
     public void resetPackageOpsNoHistory(@NonNull String packageName) {
-        mAppOpsService.resetPackageOpsNoHistory(packageName);
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "resetPackageOpsNoHistory");
+        synchronized (AppOpsService.this) {
+            final int uid = mPackageManagerInternal.getPackageUid(packageName, 0,
+                    UserHandle.getCallingUserId());
+            if (uid == Process.INVALID_UID) {
+                return;
+            }
+            UidState uidState = mUidStates.get(uid);
+            if (uidState == null || uidState.pkgOps == null) {
+                return;
+            }
+            Ops removedOps = uidState.pkgOps.remove(packageName);
+            mAppOpsCheckingService.removePackage(packageName, UserHandle.getUserId(uid));
+            if (removedOps != null) {
+                scheduleFastWriteLocked();
+            }
+        }
     }
 
     @Override
     public void setHistoryParameters(@AppOpsManager.HistoricalMode int mode,
             long baseSnapshotInterval, int compressionStep) {
-        mAppOpsService.setHistoryParameters(mode, baseSnapshotInterval, compressionStep);
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "setHistoryParameters");
+        // Must not hold the appops lock
+        mHistoricalRegistry.setHistoryParameters(mode, baseSnapshotInterval, compressionStep);
     }
 
     @Override
     public void offsetHistory(long offsetMillis) {
-        mAppOpsService.offsetHistory(offsetMillis);
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "offsetHistory");
+        // Must not hold the appops lock
+        mHistoricalRegistry.offsetHistory(offsetMillis);
+        mHistoricalRegistry.offsetDiscreteHistory(offsetMillis);
     }
 
     @Override
     public void addHistoricalOps(HistoricalOps ops) {
-        mAppOpsService.addHistoricalOps(ops);
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "addHistoricalOps");
+        // Must not hold the appops lock
+        mHistoricalRegistry.addHistoricalOps(ops);
     }
 
     @Override
     public void resetHistoryParameters() {
-        mAppOpsService.resetHistoryParameters();
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "resetHistoryParameters");
+        // Must not hold the appops lock
+        mHistoricalRegistry.resetHistoryParameters();
     }
 
     @Override
     public void clearHistory() {
-        mAppOpsService.clearHistory();
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "clearHistory");
+        // Must not hold the appops lock
+        mHistoricalRegistry.clearAllHistory();
     }
 
     @Override
     public void rebootHistory(long offlineDurationMillis) {
-        mAppOpsService.rebootHistory(offlineDurationMillis);
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "rebootHistory");
+
+        Preconditions.checkArgument(offlineDurationMillis >= 0);
+
+        // Must not hold the appops lock
+        mHistoricalRegistry.shutdown();
+
+        if (offlineDurationMillis > 0) {
+            SystemClock.sleep(offlineDurationMillis);
+        }
+
+        mHistoricalRegistry = new HistoricalRegistry(mHistoricalRegistry);
+        mHistoricalRegistry.systemReady(mContext.getContentResolver());
+        mHistoricalRegistry.persistPendingHistory();
     }
 
     /**
@@ -1998,6 +5982,24 @@
         return false;
     }
 
+    @GuardedBy("this")
+    private void removeUidsForUserLocked(int userHandle) {
+        for (int i = mUidStates.size() - 1; i >= 0; --i) {
+            final int uid = mUidStates.keyAt(i);
+            if (UserHandle.getUserId(uid) == userHandle) {
+                mUidStates.valueAt(i).clear();
+                mUidStates.removeAt(i);
+            }
+        }
+    }
+
+    private void checkSystemUid(String function) {
+        int uid = Binder.getCallingUid();
+        if (uid != Process.SYSTEM_UID) {
+            throw new SecurityException(function + " must by called by the system");
+        }
+    }
+
     private static int resolveUid(String packageName)  {
         if (packageName == null) {
             return Process.INVALID_UID;
@@ -2018,43 +6020,184 @@
         return Process.INVALID_UID;
     }
 
+    private static String[] getPackagesForUid(int uid) {
+        String[] packageNames = null;
+
+        // Very early during boot the package manager is not yet or not yet fully started. At this
+        // time there are no packages yet.
+        if (AppGlobals.getPackageManager() != null) {
+            try {
+                packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid);
+            } catch (RemoteException e) {
+                /* ignore - local call */
+            }
+        }
+        if (packageNames == null) {
+            return EmptyArray.STRING;
+        }
+        return packageNames;
+    }
+
+    private final class ClientUserRestrictionState implements DeathRecipient {
+        private final IBinder token;
+
+        ClientUserRestrictionState(IBinder token)
+                throws RemoteException {
+            token.linkToDeath(this, 0);
+            this.token = token;
+        }
+
+        public boolean setRestriction(int code, boolean restricted,
+                PackageTagsList excludedPackageTags, int userId) {
+            return mAppOpsRestrictions.setUserRestriction(token, userId, code,
+                    restricted, excludedPackageTags);
+        }
+
+        public boolean hasRestriction(int code, String packageName, String attributionTag,
+                int userId, boolean isCheckOp) {
+            return mAppOpsRestrictions.getUserRestriction(token, userId, code, packageName,
+                    attributionTag, isCheckOp);
+        }
+
+        public void removeUser(int userId) {
+            mAppOpsRestrictions.clearUserRestrictions(token, userId);
+        }
+
+        public boolean isDefault() {
+            return !mAppOpsRestrictions.hasUserRestrictions(token);
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (AppOpsService.this) {
+                mAppOpsRestrictions.clearUserRestrictions(token);
+                mOpUserRestrictions.remove(token);
+                destroy();
+            }
+        }
+
+        public void destroy() {
+            token.unlinkToDeath(this, 0);
+        }
+    }
+
+    private final class ClientGlobalRestrictionState implements DeathRecipient {
+        final IBinder mToken;
+
+        ClientGlobalRestrictionState(IBinder token)
+                throws RemoteException {
+            token.linkToDeath(this, 0);
+            this.mToken = token;
+        }
+
+        boolean setRestriction(int code, boolean restricted) {
+            return mAppOpsRestrictions.setGlobalRestriction(mToken, code, restricted);
+        }
+
+        boolean hasRestriction(int code) {
+            return mAppOpsRestrictions.getGlobalRestriction(mToken, code);
+        }
+
+        boolean isDefault() {
+            return !mAppOpsRestrictions.hasGlobalRestrictions(mToken);
+        }
+
+        @Override
+        public void binderDied() {
+            mAppOpsRestrictions.clearGlobalRestrictions(mToken);
+            mOpGlobalRestrictions.remove(mToken);
+            destroy();
+        }
+
+        void destroy() {
+            mToken.unlinkToDeath(this, 0);
+        }
+    }
+
     private final class AppOpsManagerInternalImpl extends AppOpsManagerInternal {
         @Override public void setDeviceAndProfileOwners(SparseIntArray owners) {
-            AppOpsService.this.mAppOpsService.setDeviceAndProfileOwners(owners);
+            synchronized (AppOpsService.this) {
+                mProfileOwners = owners;
+            }
         }
 
         @Override
         public void updateAppWidgetVisibility(SparseArray<String> uidPackageNames,
                 boolean visible) {
-            AppOpsService.this.mAppOpsService
-                    .updateAppWidgetVisibility(uidPackageNames, visible);
+            AppOpsService.this.updateAppWidgetVisibility(uidPackageNames, visible);
         }
 
         @Override
         public void setUidModeFromPermissionPolicy(int code, int uid, int mode,
                 @Nullable IAppOpsCallback callback) {
-            AppOpsService.this.mAppOpsService.setUidMode(code, uid, mode, callback);
+            setUidMode(code, uid, mode, callback);
         }
 
         @Override
         public void setModeFromPermissionPolicy(int code, int uid, @NonNull String packageName,
                 int mode, @Nullable IAppOpsCallback callback) {
-            AppOpsService.this.mAppOpsService
-                    .setMode(code, uid, packageName, mode, callback);
+            setMode(code, uid, packageName, mode, callback);
         }
 
 
         @Override
         public void setGlobalRestriction(int code, boolean restricted, IBinder token) {
-            AppOpsService.this.mAppOpsService
-                    .setGlobalRestriction(code, restricted, token);
+            if (Binder.getCallingPid() != Process.myPid()) {
+                // TODO instead of this enforcement put in AppOpsManagerInternal
+                throw new SecurityException("Only the system can set global restrictions");
+            }
+
+            synchronized (AppOpsService.this) {
+                ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.get(token);
+
+                if (restrictionState == null) {
+                    try {
+                        restrictionState = new ClientGlobalRestrictionState(token);
+                    } catch (RemoteException  e) {
+                        return;
+                    }
+                    mOpGlobalRestrictions.put(token, restrictionState);
+                }
+
+                if (restrictionState.setRestriction(code, restricted)) {
+                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                            AppOpsService::notifyWatchersOfChange, AppOpsService.this, code,
+                            UID_ANY));
+                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                            AppOpsService::updateStartedOpModeForUser, AppOpsService.this,
+                            code, restricted, UserHandle.USER_ALL));
+                }
+
+                if (restrictionState.isDefault()) {
+                    mOpGlobalRestrictions.remove(token);
+                    restrictionState.destroy();
+                }
+            }
         }
 
         @Override
         public int getOpRestrictionCount(int code, UserHandle user, String pkg,
                 String attributionTag) {
-            return AppOpsService.this.mAppOpsService
-                    .getOpRestrictionCount(code, user, pkg, attributionTag);
+            int number = 0;
+            synchronized (AppOpsService.this) {
+                int numRestrictions = mOpUserRestrictions.size();
+                for (int i = 0; i < numRestrictions; i++) {
+                    if (mOpUserRestrictions.valueAt(i)
+                            .hasRestriction(code, pkg, attributionTag, user.getIdentifier(),
+                                    false)) {
+                        number++;
+                    }
+                }
+
+                numRestrictions = mOpGlobalRestrictions.size();
+                for (int i = 0; i < numRestrictions; i++) {
+                    if (mOpGlobalRestrictions.valueAt(i).hasRestriction(code)) {
+                        number++;
+                    }
+                }
+            }
+
+            return number;
         }
     }
 
@@ -2350,7 +6493,7 @@
                     attributionFlags, attributionChainId, AppOpsService.this::startOperationImpl);
         }
 
-        public SyncNotedAppOp startProxyOperation(IBinder clientId, int code,
+        public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
                 boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
                 boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
@@ -2380,7 +6523,7 @@
                     proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
         }
 
-        private SyncNotedAppOp startDelegateProxyOperationImpl(IBinder clientId, int code,
+        private SyncNotedAppOp startDelegateProxyOperationImpl(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
                 boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
                 boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
@@ -2414,7 +6557,7 @@
                     AppOpsService.this::finishOperationImpl);
         }
 
-        public void finishProxyOperation(IBinder clientId, int code,
+        public void finishProxyOperation(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean skipProxyOperation) {
             if (mPolicy != null) {
                 if (mCheckOpsDelegate != null) {
@@ -2432,7 +6575,7 @@
             }
         }
 
-        private Void finishDelegateProxyOperationImpl(IBinder clientId, int code,
+        private Void finishDelegateProxyOperationImpl(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean skipProxyOperation) {
             mCheckOpsDelegate.finishProxyOperation(clientId, code, attributionSource,
                     skipProxyOperation, AppOpsService.this::finishProxyOperationImpl);
diff --git a/services/core/java/com/android/server/appop/AppOpsServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsServiceImpl.java
deleted file mode 100644
index c3d2717..0000000
--- a/services/core/java/com/android/server/appop/AppOpsServiceImpl.java
+++ /dev/null
@@ -1,4742 +0,0 @@
-/*
- * 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.appop;
-
-import static android.app.AppOpsManager.AttributedOpEntry;
-import static android.app.AppOpsManager.AttributionFlags;
-import static android.app.AppOpsManager.CALL_BACK_ON_SWITCHED_OP;
-import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG;
-import static android.app.AppOpsManager.FILTER_BY_OP_NAMES;
-import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME;
-import static android.app.AppOpsManager.FILTER_BY_UID;
-import static android.app.AppOpsManager.HISTORY_FLAG_GET_ATTRIBUTION_CHAINS;
-import static android.app.AppOpsManager.HistoricalOps;
-import static android.app.AppOpsManager.HistoricalOpsRequestFilter;
-import static android.app.AppOpsManager.KEY_BG_STATE_SETTLE_TIME;
-import static android.app.AppOpsManager.KEY_FG_SERVICE_STATE_SETTLE_TIME;
-import static android.app.AppOpsManager.KEY_TOP_STATE_SETTLE_TIME;
-import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.app.AppOpsManager.MODE_DEFAULT;
-import static android.app.AppOpsManager.MODE_ERRORED;
-import static android.app.AppOpsManager.MODE_FOREGROUND;
-import static android.app.AppOpsManager.MODE_IGNORED;
-import static android.app.AppOpsManager.Mode;
-import static android.app.AppOpsManager.OP_CAMERA;
-import static android.app.AppOpsManager.OP_FLAGS_ALL;
-import static android.app.AppOpsManager.OP_FLAG_SELF;
-import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
-import static android.app.AppOpsManager.OP_NONE;
-import static android.app.AppOpsManager.OP_PLAY_AUDIO;
-import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
-import static android.app.AppOpsManager.OP_RECORD_AUDIO;
-import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
-import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM;
-import static android.app.AppOpsManager.OP_VIBRATE;
-import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED;
-import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED;
-import static android.app.AppOpsManager.OpEntry;
-import static android.app.AppOpsManager.OpEventProxyInfo;
-import static android.app.AppOpsManager.OpFlags;
-import static android.app.AppOpsManager.RestrictionBypass;
-import static android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE;
-import static android.app.AppOpsManager._NUM_OP;
-import static android.app.AppOpsManager.extractFlagsFromKey;
-import static android.app.AppOpsManager.extractUidStateFromKey;
-import static android.app.AppOpsManager.modeToName;
-import static android.app.AppOpsManager.opAllowSystemBypassRestriction;
-import static android.app.AppOpsManager.opRestrictsRead;
-import static android.app.AppOpsManager.opToName;
-import static android.content.Intent.ACTION_PACKAGE_REMOVED;
-import static android.content.Intent.EXTRA_REPLACING;
-
-import static com.android.server.appop.AppOpsServiceImpl.ModeCallback.ALL_OPS;
-
-import android.Manifest;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.app.ActivityManager;
-import android.app.ActivityManagerInternal;
-import android.app.AppGlobals;
-import android.app.AppOpsManager;
-import android.app.admin.DevicePolicyManagerInternal;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
-import android.content.pm.PermissionInfo;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Binder;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.IBinder;
-import android.os.IBinder.DeathRecipient;
-import android.os.PackageTagsList;
-import android.os.Process;
-import android.os.RemoteCallback;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.SystemClock;
-import android.os.UserHandle;
-import android.os.storage.StorageManagerInternal;
-import android.permission.PermissionManager;
-import android.provider.Settings;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.AtomicFile;
-import android.util.KeyValueListParser;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.util.SparseIntArray;
-import android.util.TimeUtils;
-import android.util.Xml;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.app.IAppOpsActiveCallback;
-import com.android.internal.app.IAppOpsCallback;
-import com.android.internal.app.IAppOpsNotedCallback;
-import com.android.internal.app.IAppOpsStartedCallback;
-import com.android.internal.compat.IPlatformCompat;
-import com.android.internal.os.Clock;
-import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.DumpUtils;
-import com.android.internal.util.Preconditions;
-import com.android.internal.util.XmlUtils;
-import com.android.internal.util.function.pooled.PooledLambda;
-import com.android.modules.utils.TypedXmlPullParser;
-import com.android.modules.utils.TypedXmlSerializer;
-import com.android.server.LocalServices;
-import com.android.server.LockGuard;
-import com.android.server.SystemServerInitThreadPool;
-import com.android.server.pm.UserManagerInternal;
-import com.android.server.pm.permission.PermissionManagerServiceInternal;
-import com.android.server.pm.pkg.AndroidPackage;
-import com.android.server.pm.pkg.component.ParsedAttribution;
-
-import dalvik.annotation.optimization.NeverCompile;
-
-import libcore.util.EmptyArray;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-class AppOpsServiceImpl implements AppOpsServiceInterface {
-    static final String TAG = "AppOps";
-    static final boolean DEBUG = false;
-
-    /**
-     * Sentinel integer version to denote that there was no appops.xml found on boot.
-     * This will happen when a device boots with no existing userdata.
-     */
-    private static final int NO_FILE_VERSION = -2;
-
-    /**
-     * Sentinel integer version to denote that there was no version in the appops.xml found on boot.
-     * This means the file is coming from a build before versioning was added.
-     */
-    private static final int NO_VERSION = -1;
-
-    /**
-     * Increment by one every time and add the corresponding upgrade logic in
-     * {@link #upgradeLocked(int)} below. The first version was 1.
-     */
-    @VisibleForTesting
-    static final int CURRENT_VERSION = 2;
-
-    /**
-     * This stores the version of appops.xml seen at boot. If this is smaller than
-     * {@link #CURRENT_VERSION}, then we will run {@link #upgradeLocked(int)} on startup.
-     */
-    private int mVersionAtBoot = NO_FILE_VERSION;
-
-    // Write at most every 30 minutes.
-    static final long WRITE_DELAY = DEBUG ? 1000 : 30 * 60 * 1000;
-
-    // Constant meaning that any UID should be matched when dispatching callbacks
-    private static final int UID_ANY = -2;
-
-    private static final int[] OPS_RESTRICTED_ON_SUSPEND = {
-            OP_PLAY_AUDIO,
-            OP_RECORD_AUDIO,
-            OP_CAMERA,
-            OP_VIBRATE,
-    };
-    private static final int MAX_UNUSED_POOLED_OBJECTS = 3;
-
-    final Context mContext;
-    final AtomicFile mFile;
-    final Handler mHandler;
-
-    /**
-     * Pool for {@link AttributedOp.OpEventProxyInfoPool} to avoid to constantly reallocate new
-     * objects
-     */
-    @GuardedBy("this")
-    final AttributedOp.OpEventProxyInfoPool mOpEventProxyInfoPool =
-            new AttributedOp.OpEventProxyInfoPool(MAX_UNUSED_POOLED_OBJECTS);
-
-    /**
-     * Pool for {@link AttributedOp.InProgressStartOpEventPool} to avoid to constantly reallocate
-     * new objects
-     */
-    @GuardedBy("this")
-    final AttributedOp.InProgressStartOpEventPool mInProgressStartOpEventPool =
-            new AttributedOp.InProgressStartOpEventPool(mOpEventProxyInfoPool,
-                    MAX_UNUSED_POOLED_OBJECTS);
-    @Nullable
-    private final DevicePolicyManagerInternal dpmi =
-            LocalServices.getService(DevicePolicyManagerInternal.class);
-
-    private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface(
-            ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
-
-    boolean mWriteScheduled;
-    boolean mFastWriteScheduled;
-    final Runnable mWriteRunner = new Runnable() {
-        public void run() {
-            synchronized (AppOpsServiceImpl.this) {
-                mWriteScheduled = false;
-                mFastWriteScheduled = false;
-                AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
-                    @Override
-                    protected Void doInBackground(Void... params) {
-                        writeState();
-                        return null;
-                    }
-                };
-                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
-            }
-        }
-    };
-
-    @GuardedBy("this")
-    @VisibleForTesting
-    final SparseArray<UidState> mUidStates = new SparseArray<>();
-
-    volatile @NonNull HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this);
-
-    /*
-     * These are app op restrictions imposed per user from various parties.
-     */
-    private final ArrayMap<IBinder, ClientUserRestrictionState> mOpUserRestrictions =
-            new ArrayMap<>();
-
-    /*
-     * These are app op restrictions imposed globally from various parties within the system.
-     */
-    private final ArrayMap<IBinder, ClientGlobalRestrictionState> mOpGlobalRestrictions =
-            new ArrayMap<>();
-
-    SparseIntArray mProfileOwners;
-
-    /**
-     * Reverse lookup for {@link AppOpsManager#opToSwitch(int)}. Initialized once and never
-     * changed
-     */
-    private final SparseArray<int[]> mSwitchedOps = new SparseArray<>();
-
-    /**
-     * Package Manager internal. Access via {@link #getPackageManagerInternal()}
-     */
-    private @Nullable PackageManagerInternal mPackageManagerInternal;
-
-    /**
-     * Interface for app-op modes.
-     */
-    @VisibleForTesting
-    AppOpsCheckingServiceInterface mAppOpsServiceInterface;
-
-    /**
-     * Interface for app-op restrictions.
-     */
-    @VisibleForTesting
-    AppOpsRestrictions mAppOpsRestrictions;
-
-    private AppOpsUidStateTracker mUidStateTracker;
-
-    /**
-     * Hands the definition of foreground and uid states
-     */
-    @GuardedBy("this")
-    public AppOpsUidStateTracker getUidStateTracker() {
-        if (mUidStateTracker == null) {
-            mUidStateTracker = new AppOpsUidStateTrackerImpl(
-                    LocalServices.getService(ActivityManagerInternal.class),
-                    mHandler,
-                    r -> {
-                        synchronized (AppOpsServiceImpl.this) {
-                            r.run();
-                        }
-                    },
-                    Clock.SYSTEM_CLOCK, mConstants);
-
-            mUidStateTracker.addUidStateChangedCallback(new HandlerExecutor(mHandler),
-                    this::onUidStateChanged);
-        }
-        return mUidStateTracker;
-    }
-
-    /**
-     * All times are in milliseconds. These constants are kept synchronized with the system
-     * global Settings. Any access to this class or its fields should be done while
-     * holding the AppOpsService lock.
-     */
-    final class Constants extends ContentObserver {
-
-        /**
-         * How long we want for a drop in uid state from top to settle before applying it.
-         *
-         * @see Settings.Global#APP_OPS_CONSTANTS
-         * @see AppOpsManager#KEY_TOP_STATE_SETTLE_TIME
-         */
-        public long TOP_STATE_SETTLE_TIME;
-
-        /**
-         * How long we want for a drop in uid state from foreground to settle before applying it.
-         *
-         * @see Settings.Global#APP_OPS_CONSTANTS
-         * @see AppOpsManager#KEY_FG_SERVICE_STATE_SETTLE_TIME
-         */
-        public long FG_SERVICE_STATE_SETTLE_TIME;
-
-        /**
-         * How long we want for a drop in uid state from background to settle before applying it.
-         *
-         * @see Settings.Global#APP_OPS_CONSTANTS
-         * @see AppOpsManager#KEY_BG_STATE_SETTLE_TIME
-         */
-        public long BG_STATE_SETTLE_TIME;
-
-        private final KeyValueListParser mParser = new KeyValueListParser(',');
-        private ContentResolver mResolver;
-
-        Constants(Handler handler) {
-            super(handler);
-            updateConstants();
-        }
-
-        public void startMonitoring(ContentResolver resolver) {
-            mResolver = resolver;
-            mResolver.registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.APP_OPS_CONSTANTS),
-                    false, this);
-            updateConstants();
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            updateConstants();
-        }
-
-        private void updateConstants() {
-            String value = mResolver != null ? Settings.Global.getString(mResolver,
-                    Settings.Global.APP_OPS_CONSTANTS) : "";
-
-            synchronized (AppOpsServiceImpl.this) {
-                try {
-                    mParser.setString(value);
-                } catch (IllegalArgumentException e) {
-                    // Failed to parse the settings string, log this and move on
-                    // with defaults.
-                    Slog.e(TAG, "Bad app ops settings", e);
-                }
-                TOP_STATE_SETTLE_TIME = mParser.getDurationMillis(
-                        KEY_TOP_STATE_SETTLE_TIME, 5 * 1000L);
-                FG_SERVICE_STATE_SETTLE_TIME = mParser.getDurationMillis(
-                        KEY_FG_SERVICE_STATE_SETTLE_TIME, 5 * 1000L);
-                BG_STATE_SETTLE_TIME = mParser.getDurationMillis(
-                        KEY_BG_STATE_SETTLE_TIME, 1 * 1000L);
-            }
-        }
-
-        void dump(PrintWriter pw) {
-            pw.println("  Settings:");
-
-            pw.print("    ");
-            pw.print(KEY_TOP_STATE_SETTLE_TIME);
-            pw.print("=");
-            TimeUtils.formatDuration(TOP_STATE_SETTLE_TIME, pw);
-            pw.println();
-            pw.print("    ");
-            pw.print(KEY_FG_SERVICE_STATE_SETTLE_TIME);
-            pw.print("=");
-            TimeUtils.formatDuration(FG_SERVICE_STATE_SETTLE_TIME, pw);
-            pw.println();
-            pw.print("    ");
-            pw.print(KEY_BG_STATE_SETTLE_TIME);
-            pw.print("=");
-            TimeUtils.formatDuration(BG_STATE_SETTLE_TIME, pw);
-            pw.println();
-        }
-    }
-
-    @VisibleForTesting
-    final Constants mConstants;
-
-    @VisibleForTesting
-    final class UidState {
-        public final int uid;
-
-        public ArrayMap<String, Ops> pkgOps;
-
-        // true indicates there is an interested observer, false there isn't but it has such an op
-        //TODO: Move foregroundOps and hasForegroundWatchers into the AppOpsServiceInterface.
-        public SparseBooleanArray foregroundOps;
-        public boolean hasForegroundWatchers;
-
-        public UidState(int uid) {
-            this.uid = uid;
-        }
-
-        public void clear() {
-            mAppOpsServiceInterface.removeUid(uid);
-            if (pkgOps != null) {
-                for (String packageName : pkgOps.keySet()) {
-                    mAppOpsServiceInterface.removePackage(packageName, UserHandle.getUserId(uid));
-                }
-            }
-            pkgOps = null;
-        }
-
-        public boolean isDefault() {
-            boolean areAllPackageModesDefault = true;
-            if (pkgOps != null) {
-                for (String packageName : pkgOps.keySet()) {
-                    if (!mAppOpsServiceInterface.arePackageModesDefault(packageName,
-                            UserHandle.getUserId(uid))) {
-                        areAllPackageModesDefault = false;
-                        break;
-                    }
-                }
-            }
-            return (pkgOps == null || pkgOps.isEmpty())
-                    && mAppOpsServiceInterface.areUidModesDefault(uid)
-                    && areAllPackageModesDefault;
-        }
-
-        // Functions for uid mode access and manipulation.
-        public SparseIntArray getNonDefaultUidModes() {
-            return mAppOpsServiceInterface.getNonDefaultUidModes(uid);
-        }
-
-        public int getUidMode(int op) {
-            return mAppOpsServiceInterface.getUidMode(uid, op);
-        }
-
-        public boolean setUidMode(int op, int mode) {
-            return mAppOpsServiceInterface.setUidMode(uid, op, mode);
-        }
-
-        @SuppressWarnings("GuardedBy")
-        int evalMode(int op, int mode) {
-            return getUidStateTracker().evalMode(uid, op, mode);
-        }
-
-        public void evalForegroundOps() {
-            foregroundOps = null;
-            foregroundOps = mAppOpsServiceInterface.evalForegroundUidOps(uid, foregroundOps);
-            if (pkgOps != null) {
-                for (int i = pkgOps.size() - 1; i >= 0; i--) {
-                    foregroundOps = mAppOpsServiceInterface
-                            .evalForegroundPackageOps(pkgOps.valueAt(i).packageName,
-                                    foregroundOps,
-                                    UserHandle.getUserId(uid));
-                }
-            }
-            hasForegroundWatchers = false;
-            if (foregroundOps != null) {
-                for (int i = 0; i < foregroundOps.size(); i++) {
-                    if (foregroundOps.valueAt(i)) {
-                        hasForegroundWatchers = true;
-                        break;
-                    }
-                }
-            }
-        }
-
-        @SuppressWarnings("GuardedBy")
-        public int getState() {
-            return getUidStateTracker().getUidState(uid);
-        }
-
-        @SuppressWarnings("GuardedBy")
-        public void dump(PrintWriter pw, long nowElapsed) {
-            getUidStateTracker().dumpUidState(pw, uid, nowElapsed);
-        }
-    }
-
-    static final class Ops extends SparseArray<Op> {
-        final String packageName;
-        final UidState uidState;
-
-        /**
-         * The restriction properties of the package. If {@code null} it could not have been read
-         * yet and has to be refreshed.
-         */
-        @Nullable RestrictionBypass bypass;
-
-        /** Lazily populated cache of attributionTags of this package */
-        final @NonNull ArraySet<String> knownAttributionTags = new ArraySet<>();
-
-        /**
-         * Lazily populated cache of <b>valid</b> attributionTags of this package, a set smaller
-         * than or equal to {@link #knownAttributionTags}.
-         */
-        final @NonNull ArraySet<String> validAttributionTags = new ArraySet<>();
-
-        Ops(String _packageName, UidState _uidState) {
-            packageName = _packageName;
-            uidState = _uidState;
-        }
-    }
-
-    /** Returned from {@link #verifyAndGetBypass(int, String, String, String)}. */
-    private static final class PackageVerificationResult {
-
-        final RestrictionBypass bypass;
-        final boolean isAttributionTagValid;
-
-        PackageVerificationResult(RestrictionBypass bypass, boolean isAttributionTagValid) {
-            this.bypass = bypass;
-            this.isAttributionTagValid = isAttributionTagValid;
-        }
-    }
-
-    final class Op {
-        int op;
-        int uid;
-        final UidState uidState;
-        final @NonNull String packageName;
-
-        /** attributionTag -> AttributedOp */
-        final ArrayMap<String, AttributedOp> mAttributions = new ArrayMap<>(1);
-
-        Op(UidState uidState, String packageName, int op, int uid) {
-            this.op = op;
-            this.uid = uid;
-            this.uidState = uidState;
-            this.packageName = packageName;
-        }
-
-        @Mode int getMode() {
-            return mAppOpsServiceInterface.getPackageMode(packageName, this.op,
-                    UserHandle.getUserId(this.uid));
-        }
-
-        void setMode(@Mode int mode) {
-            mAppOpsServiceInterface.setPackageMode(packageName, this.op, mode,
-                    UserHandle.getUserId(this.uid));
-        }
-
-        void removeAttributionsWithNoTime() {
-            for (int i = mAttributions.size() - 1; i >= 0; i--) {
-                if (!mAttributions.valueAt(i).hasAnyTime()) {
-                    mAttributions.removeAt(i);
-                }
-            }
-        }
-
-        private @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent,
-                @Nullable String attributionTag) {
-            AttributedOp attributedOp;
-
-            attributedOp = mAttributions.get(attributionTag);
-            if (attributedOp == null) {
-                attributedOp = new AttributedOp(AppOpsServiceImpl.this, attributionTag,
-                        parent);
-                mAttributions.put(attributionTag, attributedOp);
-            }
-
-            return attributedOp;
-        }
-
-        @NonNull
-        OpEntry createEntryLocked() {
-            final int numAttributions = mAttributions.size();
-
-            final ArrayMap<String, AppOpsManager.AttributedOpEntry> attributionEntries =
-                    new ArrayMap<>(numAttributions);
-            for (int i = 0; i < numAttributions; i++) {
-                attributionEntries.put(mAttributions.keyAt(i),
-                        mAttributions.valueAt(i).createAttributedOpEntryLocked());
-            }
-
-            return new OpEntry(op, getMode(), attributionEntries);
-        }
-
-        @NonNull
-        OpEntry createSingleAttributionEntryLocked(@Nullable String attributionTag) {
-            final int numAttributions = mAttributions.size();
-
-            final ArrayMap<String, AttributedOpEntry> attributionEntries = new ArrayMap<>(1);
-            for (int i = 0; i < numAttributions; i++) {
-                if (Objects.equals(mAttributions.keyAt(i), attributionTag)) {
-                    attributionEntries.put(mAttributions.keyAt(i),
-                            mAttributions.valueAt(i).createAttributedOpEntryLocked());
-                    break;
-                }
-            }
-
-            return new OpEntry(op, getMode(), attributionEntries);
-        }
-
-        boolean isRunning() {
-            final int numAttributions = mAttributions.size();
-            for (int i = 0; i < numAttributions; i++) {
-                if (mAttributions.valueAt(i).isRunning()) {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-    }
-
-    final ArrayMap<IBinder, ModeCallback> mModeWatchers = new ArrayMap<>();
-    final ArrayMap<IBinder, SparseArray<ActiveCallback>> mActiveWatchers = new ArrayMap<>();
-    final ArrayMap<IBinder, SparseArray<StartedCallback>> mStartedWatchers = new ArrayMap<>();
-    final ArrayMap<IBinder, SparseArray<NotedCallback>> mNotedWatchers = new ArrayMap<>();
-
-    final class ModeCallback extends OnOpModeChangedListener implements DeathRecipient  {
-        /** If mWatchedOpCode==ALL_OPS notify for ops affected by the switch-op */
-        public static final int ALL_OPS = -2;
-
-        // Need to keep this only because stopWatchingMode needs an IAppOpsCallback.
-        // Otherwise we can just use the IBinder object.
-        private final IAppOpsCallback mCallback;
-
-        ModeCallback(IAppOpsCallback callback, int watchingUid, int flags, int watchedOpCode,
-                int callingUid, int callingPid) {
-            super(watchingUid, flags, watchedOpCode, callingUid, callingPid);
-            this.mCallback = callback;
-            try {
-                mCallback.asBinder().linkToDeath(this, 0);
-            } catch (RemoteException e) {
-                /*ignored*/
-            }
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("ModeCallback{");
-            sb.append(Integer.toHexString(System.identityHashCode(this)));
-            sb.append(" watchinguid=");
-            UserHandle.formatUid(sb, getWatchingUid());
-            sb.append(" flags=0x");
-            sb.append(Integer.toHexString(getFlags()));
-            switch (getWatchedOpCode()) {
-                case OP_NONE:
-                    break;
-                case ALL_OPS:
-                    sb.append(" op=(all)");
-                    break;
-                default:
-                    sb.append(" op=");
-                    sb.append(opToName(getWatchedOpCode()));
-                    break;
-            }
-            sb.append(" from uid=");
-            UserHandle.formatUid(sb, getCallingUid());
-            sb.append(" pid=");
-            sb.append(getCallingPid());
-            sb.append('}');
-            return sb.toString();
-        }
-
-        void unlinkToDeath() {
-            mCallback.asBinder().unlinkToDeath(this, 0);
-        }
-
-        @Override
-        public void binderDied() {
-            stopWatchingMode(mCallback);
-        }
-
-        @Override
-        public void onOpModeChanged(int op, int uid, String packageName) throws RemoteException {
-            mCallback.opChanged(op, uid, packageName);
-        }
-    }
-
-    final class ActiveCallback implements DeathRecipient {
-        final IAppOpsActiveCallback mCallback;
-        final int mWatchingUid;
-        final int mCallingUid;
-        final int mCallingPid;
-
-        ActiveCallback(IAppOpsActiveCallback callback, int watchingUid, int callingUid,
-                int callingPid) {
-            mCallback = callback;
-            mWatchingUid = watchingUid;
-            mCallingUid = callingUid;
-            mCallingPid = callingPid;
-            try {
-                mCallback.asBinder().linkToDeath(this, 0);
-            } catch (RemoteException e) {
-                /*ignored*/
-            }
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("ActiveCallback{");
-            sb.append(Integer.toHexString(System.identityHashCode(this)));
-            sb.append(" watchinguid=");
-            UserHandle.formatUid(sb, mWatchingUid);
-            sb.append(" from uid=");
-            UserHandle.formatUid(sb, mCallingUid);
-            sb.append(" pid=");
-            sb.append(mCallingPid);
-            sb.append('}');
-            return sb.toString();
-        }
-
-        void destroy() {
-            mCallback.asBinder().unlinkToDeath(this, 0);
-        }
-
-        @Override
-        public void binderDied() {
-            stopWatchingActive(mCallback);
-        }
-    }
-
-    final class StartedCallback implements DeathRecipient {
-        final IAppOpsStartedCallback mCallback;
-        final int mWatchingUid;
-        final int mCallingUid;
-        final int mCallingPid;
-
-        StartedCallback(IAppOpsStartedCallback callback, int watchingUid, int callingUid,
-                int callingPid) {
-            mCallback = callback;
-            mWatchingUid = watchingUid;
-            mCallingUid = callingUid;
-            mCallingPid = callingPid;
-            try {
-                mCallback.asBinder().linkToDeath(this, 0);
-            } catch (RemoteException e) {
-                /*ignored*/
-            }
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("StartedCallback{");
-            sb.append(Integer.toHexString(System.identityHashCode(this)));
-            sb.append(" watchinguid=");
-            UserHandle.formatUid(sb, mWatchingUid);
-            sb.append(" from uid=");
-            UserHandle.formatUid(sb, mCallingUid);
-            sb.append(" pid=");
-            sb.append(mCallingPid);
-            sb.append('}');
-            return sb.toString();
-        }
-
-        void destroy() {
-            mCallback.asBinder().unlinkToDeath(this, 0);
-        }
-
-        @Override
-        public void binderDied() {
-            stopWatchingStarted(mCallback);
-        }
-    }
-
-    final class NotedCallback implements DeathRecipient {
-        final IAppOpsNotedCallback mCallback;
-        final int mWatchingUid;
-        final int mCallingUid;
-        final int mCallingPid;
-
-        NotedCallback(IAppOpsNotedCallback callback, int watchingUid, int callingUid,
-                int callingPid) {
-            mCallback = callback;
-            mWatchingUid = watchingUid;
-            mCallingUid = callingUid;
-            mCallingPid = callingPid;
-            try {
-                mCallback.asBinder().linkToDeath(this, 0);
-            } catch (RemoteException e) {
-                /*ignored*/
-            }
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("NotedCallback{");
-            sb.append(Integer.toHexString(System.identityHashCode(this)));
-            sb.append(" watchinguid=");
-            UserHandle.formatUid(sb, mWatchingUid);
-            sb.append(" from uid=");
-            UserHandle.formatUid(sb, mCallingUid);
-            sb.append(" pid=");
-            sb.append(mCallingPid);
-            sb.append('}');
-            return sb.toString();
-        }
-
-        void destroy() {
-            mCallback.asBinder().unlinkToDeath(this, 0);
-        }
-
-        @Override
-        public void binderDied() {
-            stopWatchingNoted(mCallback);
-        }
-    }
-
-    /**
-     * Call {@link AttributedOp#onClientDeath attributedOp.onClientDeath(clientId)}.
-     */
-    static void onClientDeath(@NonNull AttributedOp attributedOp,
-            @NonNull IBinder clientId) {
-        attributedOp.onClientDeath(clientId);
-    }
-
-    AppOpsServiceImpl(File storagePath, Handler handler, Context context) {
-        mContext = context;
-
-        for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) {
-            int switchCode = AppOpsManager.opToSwitch(switchedCode);
-            mSwitchedOps.put(switchCode,
-                    ArrayUtils.appendInt(mSwitchedOps.get(switchCode), switchedCode));
-        }
-        mAppOpsServiceInterface = new AppOpsCheckingServiceTracingDecorator(
-                new AppOpsCheckingServiceImpl(this, this, handler, context, mSwitchedOps));
-        mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler,
-                mAppOpsServiceInterface);
-
-        LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
-        mFile = new AtomicFile(storagePath, "appops");
-
-        mHandler = handler;
-        mConstants = new Constants(mHandler);
-        readState();
-    }
-
-    /**
-     * Handler for work when packages are removed or updated
-     */
-    private BroadcastReceiver mOnPackageUpdatedReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            String pkgName = intent.getData().getEncodedSchemeSpecificPart();
-            int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
-
-            if (action.equals(ACTION_PACKAGE_REMOVED) && !intent.hasExtra(EXTRA_REPLACING)) {
-                synchronized (AppOpsServiceImpl.this) {
-                    UidState uidState = mUidStates.get(uid);
-                    if (uidState == null || uidState.pkgOps == null) {
-                        return;
-                    }
-                    mAppOpsServiceInterface.removePackage(pkgName, UserHandle.getUserId(uid));
-                    Ops removedOps = uidState.pkgOps.remove(pkgName);
-                    if (removedOps != null) {
-                        scheduleFastWriteLocked();
-                    }
-                }
-            } else if (action.equals(Intent.ACTION_PACKAGE_REPLACED)) {
-                AndroidPackage pkg = getPackageManagerInternal().getPackage(pkgName);
-                if (pkg == null) {
-                    return;
-                }
-
-                ArrayMap<String, String> dstAttributionTags = new ArrayMap<>();
-                ArraySet<String> attributionTags = new ArraySet<>();
-                attributionTags.add(null);
-                if (pkg.getAttributions() != null) {
-                    int numAttributions = pkg.getAttributions().size();
-                    for (int attributionNum = 0; attributionNum < numAttributions;
-                            attributionNum++) {
-                        ParsedAttribution attribution = pkg.getAttributions().get(attributionNum);
-                        attributionTags.add(attribution.getTag());
-
-                        int numInheritFrom = attribution.getInheritFrom().size();
-                        for (int inheritFromNum = 0; inheritFromNum < numInheritFrom;
-                                inheritFromNum++) {
-                            dstAttributionTags.put(attribution.getInheritFrom().get(inheritFromNum),
-                                    attribution.getTag());
-                        }
-                    }
-                }
-
-                synchronized (AppOpsServiceImpl.this) {
-                    UidState uidState = mUidStates.get(uid);
-                    if (uidState == null || uidState.pkgOps == null) {
-                        return;
-                    }
-
-                    Ops ops = uidState.pkgOps.get(pkgName);
-                    if (ops == null) {
-                        return;
-                    }
-
-                    // Reset cached package properties to re-initialize when needed
-                    ops.bypass = null;
-                    ops.knownAttributionTags.clear();
-
-                    // Merge data collected for removed attributions into their successor
-                    // attributions
-                    int numOps = ops.size();
-                    for (int opNum = 0; opNum < numOps; opNum++) {
-                        Op op = ops.valueAt(opNum);
-
-                        int numAttributions = op.mAttributions.size();
-                        for (int attributionNum = numAttributions - 1; attributionNum >= 0;
-                                attributionNum--) {
-                            String attributionTag = op.mAttributions.keyAt(attributionNum);
-
-                            if (attributionTags.contains(attributionTag)) {
-                                // attribution still exist after upgrade
-                                continue;
-                            }
-
-                            String newAttributionTag = dstAttributionTags.get(attributionTag);
-
-                            AttributedOp newAttributedOp = op.getOrCreateAttribution(op,
-                                    newAttributionTag);
-                            newAttributedOp.add(op.mAttributions.valueAt(attributionNum));
-                            op.mAttributions.removeAt(attributionNum);
-
-                            scheduleFastWriteLocked();
-                        }
-                    }
-                }
-            }
-        }
-    };
-
-    @Override
-    public void systemReady() {
-        synchronized (this) {
-            upgradeLocked(mVersionAtBoot);
-        }
-
-        mConstants.startMonitoring(mContext.getContentResolver());
-        mHistoricalRegistry.systemReady(mContext.getContentResolver());
-
-        IntentFilter packageUpdateFilter = new IntentFilter();
-        packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
-        packageUpdateFilter.addDataScheme("package");
-
-        mContext.registerReceiverAsUser(mOnPackageUpdatedReceiver, UserHandle.ALL,
-                packageUpdateFilter, null, null);
-
-        synchronized (this) {
-            for (int uidNum = mUidStates.size() - 1; uidNum >= 0; uidNum--) {
-                int uid = mUidStates.keyAt(uidNum);
-                UidState uidState = mUidStates.valueAt(uidNum);
-
-                String[] pkgsInUid = getPackagesForUid(uidState.uid);
-                if (ArrayUtils.isEmpty(pkgsInUid)) {
-                    uidState.clear();
-                    mUidStates.removeAt(uidNum);
-                    scheduleFastWriteLocked();
-                    continue;
-                }
-
-                ArrayMap<String, Ops> pkgs = uidState.pkgOps;
-                if (pkgs == null) {
-                    continue;
-                }
-
-                int numPkgs = pkgs.size();
-                for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
-                    String pkg = pkgs.keyAt(pkgNum);
-
-                    String action;
-                    if (!ArrayUtils.contains(pkgsInUid, pkg)) {
-                        action = Intent.ACTION_PACKAGE_REMOVED;
-                    } else {
-                        action = Intent.ACTION_PACKAGE_REPLACED;
-                    }
-
-                    SystemServerInitThreadPool.submit(
-                            () -> mOnPackageUpdatedReceiver.onReceive(mContext, new Intent(action)
-                                    .setData(Uri.fromParts("package", pkg, null))
-                                    .putExtra(Intent.EXTRA_UID, uid)),
-                            "Update app-ops uidState in case package " + pkg + " changed");
-                }
-            }
-        }
-
-        final IntentFilter packageSuspendFilter = new IntentFilter();
-        packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
-        packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
-        mContext.registerReceiverAsUser(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                final int[] changedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST);
-                final String[] changedPkgs = intent.getStringArrayExtra(
-                        Intent.EXTRA_CHANGED_PACKAGE_LIST);
-                for (int code : OPS_RESTRICTED_ON_SUSPEND) {
-                    ArraySet<OnOpModeChangedListener> onModeChangedListeners;
-                    synchronized (AppOpsServiceImpl.this) {
-                        onModeChangedListeners =
-                                mAppOpsServiceInterface.getOpModeChangedListeners(code);
-                        if (onModeChangedListeners == null) {
-                            continue;
-                        }
-                    }
-                    for (int i = 0; i < changedUids.length; i++) {
-                        final int changedUid = changedUids[i];
-                        final String changedPkg = changedPkgs[i];
-                        // We trust packagemanager to insert matching uid and packageNames in the
-                        // extras
-                        notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg);
-                    }
-                }
-            }
-        }, UserHandle.ALL, packageSuspendFilter, null, null);
-    }
-
-    @Override
-    public void packageRemoved(int uid, String packageName) {
-        synchronized (this) {
-            UidState uidState = mUidStates.get(uid);
-            if (uidState == null) {
-                return;
-            }
-
-            Ops removedOps = null;
-
-            // Remove any package state if such.
-            if (uidState.pkgOps != null) {
-                removedOps = uidState.pkgOps.remove(packageName);
-                mAppOpsServiceInterface.removePackage(packageName, UserHandle.getUserId(uid));
-            }
-
-            // If we just nuked the last package state check if the UID is valid.
-            if (removedOps != null && uidState.pkgOps.isEmpty()
-                    && getPackagesForUid(uid).length <= 0) {
-                uidState.clear();
-                mUidStates.remove(uid);
-            }
-
-            if (removedOps != null) {
-                scheduleFastWriteLocked();
-
-                final int numOps = removedOps.size();
-                for (int opNum = 0; opNum < numOps; opNum++) {
-                    final Op op = removedOps.valueAt(opNum);
-
-                    final int numAttributions = op.mAttributions.size();
-                    for (int attributionNum = 0; attributionNum < numAttributions;
-                            attributionNum++) {
-                        AttributedOp attributedOp = op.mAttributions.valueAt(attributionNum);
-
-                        while (attributedOp.isRunning()) {
-                            attributedOp.finished(attributedOp.mInProgressEvents.keyAt(0));
-                        }
-                        while (attributedOp.isPaused()) {
-                            attributedOp.finished(attributedOp.mPausedInProgressEvents.keyAt(0));
-                        }
-                    }
-                }
-            }
-        }
-
-        mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory,
-                mHistoricalRegistry, uid, packageName));
-    }
-
-    @Override
-    public void uidRemoved(int uid) {
-        synchronized (this) {
-            if (mUidStates.indexOfKey(uid) >= 0) {
-                mUidStates.get(uid).clear();
-                mUidStates.remove(uid);
-                scheduleFastWriteLocked();
-            }
-        }
-    }
-
-    // The callback method from ForegroundPolicyInterface
-    private void onUidStateChanged(int uid, int state, boolean foregroundModeMayChange) {
-        synchronized (this) {
-            UidState uidState = getUidStateLocked(uid, true);
-
-            if (uidState != null && foregroundModeMayChange && uidState.hasForegroundWatchers) {
-                for (int fgi = uidState.foregroundOps.size() - 1; fgi >= 0; fgi--) {
-                    if (!uidState.foregroundOps.valueAt(fgi)) {
-                        continue;
-                    }
-                    final int code = uidState.foregroundOps.keyAt(fgi);
-
-                    if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)
-                            && uidState.getUidMode(code) == AppOpsManager.MODE_FOREGROUND) {
-                        mHandler.sendMessage(PooledLambda.obtainMessage(
-                                AppOpsServiceImpl::notifyOpChangedForAllPkgsInUid,
-                                this, code, uidState.uid, true, null));
-                    } else if (uidState.pkgOps != null) {
-                        final ArraySet<OnOpModeChangedListener> listenerSet =
-                                mAppOpsServiceInterface.getOpModeChangedListeners(code);
-                        if (listenerSet != null) {
-                            for (int cbi = listenerSet.size() - 1; cbi >= 0; cbi--) {
-                                final OnOpModeChangedListener listener = listenerSet.valueAt(cbi);
-                                if ((listener.getFlags()
-                                        & AppOpsManager.WATCH_FOREGROUND_CHANGES) == 0
-                                        || !listener.isWatchingUid(uidState.uid)) {
-                                    continue;
-                                }
-                                for (int pkgi = uidState.pkgOps.size() - 1; pkgi >= 0; pkgi--) {
-                                    final Op op = uidState.pkgOps.valueAt(pkgi).get(code);
-                                    if (op == null) {
-                                        continue;
-                                    }
-                                    if (op.getMode() == AppOpsManager.MODE_FOREGROUND) {
-                                        mHandler.sendMessage(PooledLambda.obtainMessage(
-                                                AppOpsServiceImpl::notifyOpChanged,
-                                                this, listenerSet.valueAt(cbi), code, uidState.uid,
-                                                uidState.pkgOps.keyAt(pkgi)));
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-
-            if (uidState != null && uidState.pkgOps != null) {
-                int numPkgs = uidState.pkgOps.size();
-                for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
-                    Ops ops = uidState.pkgOps.valueAt(pkgNum);
-
-                    int numOps = ops.size();
-                    for (int opNum = 0; opNum < numOps; opNum++) {
-                        Op op = ops.valueAt(opNum);
-
-                        int numAttributions = op.mAttributions.size();
-                        for (int attributionNum = 0; attributionNum < numAttributions;
-                                attributionNum++) {
-                            AttributedOp attributedOp = op.mAttributions.valueAt(
-                                    attributionNum);
-
-                            attributedOp.onUidStateChanged(state);
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Notify the proc state or capability has changed for a certain UID.
-     */
-    @Override
-    public void updateUidProcState(int uid, int procState,
-            @ActivityManager.ProcessCapability int capability) {
-        synchronized (this) {
-            getUidStateTracker().updateUidProcState(uid, procState, capability);
-            if (!mUidStates.contains(uid)) {
-                UidState uidState = new UidState(uid);
-                mUidStates.put(uid, uidState);
-                onUidStateChanged(uid,
-                        AppOpsUidStateTracker.processStateToUidState(procState), false);
-            }
-        }
-    }
-
-    @Override
-    public void shutdown() {
-        Slog.w(TAG, "Writing app ops before shutdown...");
-        boolean doWrite = false;
-        synchronized (this) {
-            if (mWriteScheduled) {
-                mWriteScheduled = false;
-                mFastWriteScheduled = false;
-                mHandler.removeCallbacks(mWriteRunner);
-                doWrite = true;
-            }
-        }
-        if (doWrite) {
-            writeState();
-        }
-
-        mHistoricalRegistry.shutdown();
-    }
-
-    private ArrayList<AppOpsManager.OpEntry> collectOps(Ops pkgOps, int[] ops) {
-        ArrayList<AppOpsManager.OpEntry> resOps = null;
-        if (ops == null) {
-            resOps = new ArrayList<>();
-            for (int j = 0; j < pkgOps.size(); j++) {
-                Op curOp = pkgOps.valueAt(j);
-                resOps.add(getOpEntryForResult(curOp));
-            }
-        } else {
-            for (int j = 0; j < ops.length; j++) {
-                Op curOp = pkgOps.get(ops[j]);
-                if (curOp != null) {
-                    if (resOps == null) {
-                        resOps = new ArrayList<>();
-                    }
-                    resOps.add(getOpEntryForResult(curOp));
-                }
-            }
-        }
-        return resOps;
-    }
-
-    @Nullable
-    private ArrayList<AppOpsManager.OpEntry> collectUidOps(@NonNull UidState uidState,
-            @Nullable int[] ops) {
-        final SparseIntArray opModes = uidState.getNonDefaultUidModes();
-        if (opModes == null) {
-            return null;
-        }
-
-        int opModeCount = opModes.size();
-        if (opModeCount == 0) {
-            return null;
-        }
-        ArrayList<AppOpsManager.OpEntry> resOps = null;
-        if (ops == null) {
-            resOps = new ArrayList<>();
-            for (int i = 0; i < opModeCount; i++) {
-                int code = opModes.keyAt(i);
-                resOps.add(new OpEntry(code, opModes.get(code), Collections.emptyMap()));
-            }
-        } else {
-            for (int j = 0; j < ops.length; j++) {
-                int code = ops[j];
-                if (opModes.indexOfKey(code) >= 0) {
-                    if (resOps == null) {
-                        resOps = new ArrayList<>();
-                    }
-                    resOps.add(new OpEntry(code, opModes.get(code), Collections.emptyMap()));
-                }
-            }
-        }
-        return resOps;
-    }
-
-    private static @NonNull OpEntry getOpEntryForResult(@NonNull Op op) {
-        return op.createEntryLocked();
-    }
-
-    @Override
-    public List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops) {
-        final int callingUid = Binder.getCallingUid();
-        final boolean hasAllPackageAccess = mContext.checkPermission(
-                Manifest.permission.GET_APP_OPS_STATS, Binder.getCallingPid(),
-                Binder.getCallingUid(), null) == PackageManager.PERMISSION_GRANTED;
-        ArrayList<AppOpsManager.PackageOps> res = null;
-        synchronized (this) {
-            final int uidStateCount = mUidStates.size();
-            for (int i = 0; i < uidStateCount; i++) {
-                UidState uidState = mUidStates.valueAt(i);
-                if (uidState.pkgOps == null || uidState.pkgOps.isEmpty()) {
-                    continue;
-                }
-                ArrayMap<String, Ops> packages = uidState.pkgOps;
-                final int packageCount = packages.size();
-                for (int j = 0; j < packageCount; j++) {
-                    Ops pkgOps = packages.valueAt(j);
-                    ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops);
-                    if (resOps != null) {
-                        if (res == null) {
-                            res = new ArrayList<>();
-                        }
-                        AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
-                                pkgOps.packageName, pkgOps.uidState.uid, resOps);
-                        // Caller can always see their packages and with a permission all.
-                        if (hasAllPackageAccess || callingUid == pkgOps.uidState.uid) {
-                            res.add(resPackage);
-                        }
-                    }
-                }
-            }
-        }
-        return res;
-    }
-
-    @Override
-    public List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName,
-            int[] ops) {
-        enforceGetAppOpsStatsPermissionIfNeeded(uid, packageName);
-        String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
-        if (resolvedPackageName == null) {
-            return Collections.emptyList();
-        }
-        synchronized (this) {
-            Ops pkgOps = getOpsLocked(uid, resolvedPackageName, null, false, null,
-                    /* edit */ false);
-            if (pkgOps == null) {
-                return null;
-            }
-            ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops);
-            if (resOps == null) {
-                return null;
-            }
-            ArrayList<AppOpsManager.PackageOps> res = new ArrayList<>();
-            AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
-                    pkgOps.packageName, pkgOps.uidState.uid, resOps);
-            res.add(resPackage);
-            return res;
-        }
-    }
-
-    private void enforceGetAppOpsStatsPermissionIfNeeded(int uid, String packageName) {
-        final int callingUid = Binder.getCallingUid();
-        // We get to access everything
-        if (callingUid == Process.myPid()) {
-            return;
-        }
-        // Apps can access their own data
-        if (uid == callingUid && packageName != null
-                && checkPackage(uid, packageName) == MODE_ALLOWED) {
-            return;
-        }
-        // Otherwise, you need a permission...
-        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
-                Binder.getCallingPid(), callingUid, null);
-    }
-
-    /**
-     * Verify that historical appop request arguments are valid.
-     */
-    private void ensureHistoricalOpRequestIsValid(int uid, String packageName,
-            String attributionTag, List<String> opNames, int filter, long beginTimeMillis,
-            long endTimeMillis, int flags) {
-        if ((filter & FILTER_BY_UID) != 0) {
-            Preconditions.checkArgument(uid != Process.INVALID_UID);
-        } else {
-            Preconditions.checkArgument(uid == Process.INVALID_UID);
-        }
-
-        if ((filter & FILTER_BY_PACKAGE_NAME) != 0) {
-            Objects.requireNonNull(packageName);
-        } else {
-            Preconditions.checkArgument(packageName == null);
-        }
-
-        if ((filter & FILTER_BY_ATTRIBUTION_TAG) == 0) {
-            Preconditions.checkArgument(attributionTag == null);
-        }
-
-        if ((filter & FILTER_BY_OP_NAMES) != 0) {
-            Objects.requireNonNull(opNames);
-        } else {
-            Preconditions.checkArgument(opNames == null);
-        }
-
-        Preconditions.checkFlagsArgument(filter,
-                FILTER_BY_UID | FILTER_BY_PACKAGE_NAME | FILTER_BY_ATTRIBUTION_TAG
-                        | FILTER_BY_OP_NAMES);
-        Preconditions.checkArgumentNonnegative(beginTimeMillis);
-        Preconditions.checkArgument(endTimeMillis > beginTimeMillis);
-        Preconditions.checkFlagsArgument(flags, OP_FLAGS_ALL);
-    }
-
-    @Override
-    public void getHistoricalOps(int uid, String packageName, String attributionTag,
-            List<String> opNames, int dataType, int filter, long beginTimeMillis,
-            long endTimeMillis, int flags, RemoteCallback callback) {
-        PackageManager pm = mContext.getPackageManager();
-
-        ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter,
-                beginTimeMillis, endTimeMillis, flags);
-        Objects.requireNonNull(callback, "callback cannot be null");
-        ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class);
-        boolean isSelfRequest = (filter & FILTER_BY_UID) != 0 && uid == Binder.getCallingUid();
-        if (!isSelfRequest) {
-            boolean isCallerInstrumented =
-                    ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID;
-            boolean isCallerSystem = Binder.getCallingPid() == Process.myPid();
-            boolean isCallerPermissionController;
-            try {
-                isCallerPermissionController = pm.getPackageUidAsUser(
-                        mContext.getPackageManager().getPermissionControllerPackageName(), 0,
-                        UserHandle.getUserId(Binder.getCallingUid()))
-                        == Binder.getCallingUid();
-            } catch (PackageManager.NameNotFoundException doesNotHappen) {
-                return;
-            }
-
-            boolean doesCallerHavePermission = mContext.checkPermission(
-                    android.Manifest.permission.GET_HISTORICAL_APP_OPS_STATS,
-                    Binder.getCallingPid(), Binder.getCallingUid())
-                    == PackageManager.PERMISSION_GRANTED;
-
-            if (!isCallerSystem && !isCallerInstrumented && !isCallerPermissionController
-                    && !doesCallerHavePermission) {
-                mHandler.post(() -> callback.sendResult(new Bundle()));
-                return;
-            }
-
-            mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
-                    Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps");
-        }
-
-        final String[] opNamesArray = (opNames != null)
-                ? opNames.toArray(new String[opNames.size()]) : null;
-
-        Set<String> attributionChainExemptPackages = null;
-        if ((dataType & HISTORY_FLAG_GET_ATTRIBUTION_CHAINS) != 0) {
-            attributionChainExemptPackages =
-                    PermissionManager.getIndicatorExemptedPackages(mContext);
-        }
-
-        final String[] chainExemptPkgArray = attributionChainExemptPackages != null
-                ? attributionChainExemptPackages.toArray(
-                new String[attributionChainExemptPackages.size()]) : null;
-
-        // Must not hold the appops lock
-        mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps,
-                mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType,
-                filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray,
-                callback).recycleOnUse());
-    }
-
-    @Override
-    public void getHistoricalOpsFromDiskRaw(int uid, String packageName, String attributionTag,
-            List<String> opNames, int dataType, int filter, long beginTimeMillis,
-            long endTimeMillis, int flags, RemoteCallback callback) {
-        ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter,
-                beginTimeMillis, endTimeMillis, flags);
-        Objects.requireNonNull(callback, "callback cannot be null");
-
-        mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS,
-                Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps");
-
-        final String[] opNamesArray = (opNames != null)
-                ? opNames.toArray(new String[opNames.size()]) : null;
-
-        Set<String> attributionChainExemptPackages = null;
-        if ((dataType & HISTORY_FLAG_GET_ATTRIBUTION_CHAINS) != 0) {
-            attributionChainExemptPackages =
-                    PermissionManager.getIndicatorExemptedPackages(mContext);
-        }
-
-        final String[] chainExemptPkgArray = attributionChainExemptPackages != null
-                ? attributionChainExemptPackages.toArray(
-                new String[attributionChainExemptPackages.size()]) : null;
-
-        // Must not hold the appops lock
-        mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOpsFromDiskRaw,
-                mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType,
-                filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray,
-                callback).recycleOnUse());
-    }
-
-    @Override
-    public void reloadNonHistoricalState() {
-        mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS,
-                Binder.getCallingPid(), Binder.getCallingUid(), "reloadNonHistoricalState");
-        writeState();
-        readState();
-    }
-
-    @Override
-    public List<AppOpsManager.PackageOps> getUidOps(int uid, int[] ops) {
-        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
-                Binder.getCallingPid(), Binder.getCallingUid(), null);
-        synchronized (this) {
-            UidState uidState = getUidStateLocked(uid, false);
-            if (uidState == null) {
-                return null;
-            }
-            ArrayList<AppOpsManager.OpEntry> resOps = collectUidOps(uidState, ops);
-            if (resOps == null) {
-                return null;
-            }
-            ArrayList<AppOpsManager.PackageOps> res = new ArrayList<AppOpsManager.PackageOps>();
-            AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
-                    null, uidState.uid, resOps);
-            res.add(resPackage);
-            return res;
-        }
-    }
-
-    private void pruneOpLocked(Op op, int uid, String packageName) {
-        op.removeAttributionsWithNoTime();
-
-        if (op.mAttributions.isEmpty()) {
-            Ops ops = getOpsLocked(uid, packageName, null, false, null, /* edit */ false);
-            if (ops != null) {
-                ops.remove(op.op);
-                op.setMode(AppOpsManager.opToDefaultMode(op.op));
-                if (ops.size() <= 0) {
-                    UidState uidState = ops.uidState;
-                    ArrayMap<String, Ops> pkgOps = uidState.pkgOps;
-                    if (pkgOps != null) {
-                        pkgOps.remove(ops.packageName);
-                        mAppOpsServiceInterface.removePackage(ops.packageName,
-                                UserHandle.getUserId(uidState.uid));
-                        if (pkgOps.isEmpty()) {
-                            uidState.pkgOps = null;
-                        }
-                        if (uidState.isDefault()) {
-                            uidState.clear();
-                            mUidStates.remove(uid);
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    @Override
-    public void enforceManageAppOpsModes(int callingPid, int callingUid, int targetUid) {
-        if (callingPid == Process.myPid()) {
-            return;
-        }
-        final int callingUser = UserHandle.getUserId(callingUid);
-        synchronized (this) {
-            if (mProfileOwners != null && mProfileOwners.get(callingUser, -1) == callingUid) {
-                if (targetUid >= 0 && callingUser == UserHandle.getUserId(targetUid)) {
-                    // Profile owners are allowed to change modes but only for apps
-                    // within their user.
-                    return;
-                }
-            }
-        }
-        mContext.enforcePermission(android.Manifest.permission.MANAGE_APP_OPS_MODES,
-                Binder.getCallingPid(), Binder.getCallingUid(), null);
-    }
-
-    @Override
-    public void setUidMode(int code, int uid, int mode,
-            @Nullable IAppOpsCallback permissionPolicyCallback) {
-        if (DEBUG) {
-            Slog.i(TAG, "uid " + uid + " OP_" + opToName(code) + " := " + modeToName(mode)
-                    + " by uid " + Binder.getCallingUid());
-        }
-
-        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid);
-        verifyIncomingOp(code);
-        code = AppOpsManager.opToSwitch(code);
-
-        if (permissionPolicyCallback == null) {
-            updatePermissionRevokedCompat(uid, code, mode);
-        }
-
-        int previousMode;
-        synchronized (this) {
-            final int defaultMode = AppOpsManager.opToDefaultMode(code);
-
-            UidState uidState = getUidStateLocked(uid, false);
-            if (uidState == null) {
-                if (mode == defaultMode) {
-                    return;
-                }
-                uidState = new UidState(uid);
-                mUidStates.put(uid, uidState);
-            }
-            if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) {
-                previousMode = uidState.getUidMode(code);
-            } else {
-                // doesn't look right but is legacy behavior.
-                previousMode = MODE_DEFAULT;
-            }
-
-            if (!uidState.setUidMode(code, mode)) {
-                return;
-            }
-            uidState.evalForegroundOps();
-            if (mode != MODE_ERRORED && mode != previousMode) {
-                updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
-            }
-        }
-
-        notifyOpChangedForAllPkgsInUid(code, uid, false, permissionPolicyCallback);
-        notifyOpChangedSync(code, uid, null, mode, previousMode);
-    }
-
-    /**
-     * Notify that an op changed for all packages in an uid.
-     *
-     * @param code           The op that changed
-     * @param uid            The uid the op was changed for
-     * @param onlyForeground Only notify watchers that watch for foreground changes
-     */
-    private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground,
-            @Nullable IAppOpsCallback callbackToIgnore) {
-        ModeCallback listenerToIgnore = callbackToIgnore != null
-                ? mModeWatchers.get(callbackToIgnore.asBinder()) : null;
-        mAppOpsServiceInterface.notifyOpChangedForAllPkgsInUid(code, uid, onlyForeground,
-                listenerToIgnore);
-    }
-
-    private void updatePermissionRevokedCompat(int uid, int switchCode, int mode) {
-        PackageManager packageManager = mContext.getPackageManager();
-        if (packageManager == null) {
-            // This can only happen during early boot. At this time the permission state and appop
-            // state are in sync
-            return;
-        }
-
-        String[] packageNames = packageManager.getPackagesForUid(uid);
-        if (ArrayUtils.isEmpty(packageNames)) {
-            return;
-        }
-        String packageName = packageNames[0];
-
-        int[] ops = mSwitchedOps.get(switchCode);
-        for (int code : ops) {
-            String permissionName = AppOpsManager.opToPermission(code);
-            if (permissionName == null) {
-                continue;
-            }
-
-            if (packageManager.checkPermission(permissionName, packageName)
-                    != PackageManager.PERMISSION_GRANTED) {
-                continue;
-            }
-
-            PermissionInfo permissionInfo;
-            try {
-                permissionInfo = packageManager.getPermissionInfo(permissionName, 0);
-            } catch (PackageManager.NameNotFoundException e) {
-                e.printStackTrace();
-                continue;
-            }
-
-            if (!permissionInfo.isRuntime()) {
-                continue;
-            }
-
-            boolean supportsRuntimePermissions = getPackageManagerInternal()
-                    .getUidTargetSdkVersion(uid) >= Build.VERSION_CODES.M;
-
-            UserHandle user = UserHandle.getUserHandleForUid(uid);
-            boolean isRevokedCompat;
-            if (permissionInfo.backgroundPermission != null) {
-                if (packageManager.checkPermission(permissionInfo.backgroundPermission, packageName)
-                        == PackageManager.PERMISSION_GRANTED) {
-                    boolean isBackgroundRevokedCompat = mode != AppOpsManager.MODE_ALLOWED;
-
-                    if (isBackgroundRevokedCompat && supportsRuntimePermissions) {
-                        Slog.w(TAG, "setUidMode() called with a mode inconsistent with runtime"
-                                + " permission state, this is discouraged and you should revoke the"
-                                + " runtime permission instead: uid=" + uid + ", switchCode="
-                                + switchCode + ", mode=" + mode + ", permission="
-                                + permissionInfo.backgroundPermission);
-                    }
-
-                    final long identity = Binder.clearCallingIdentity();
-                    try {
-                        packageManager.updatePermissionFlags(permissionInfo.backgroundPermission,
-                                packageName, PackageManager.FLAG_PERMISSION_REVOKED_COMPAT,
-                                isBackgroundRevokedCompat
-                                        ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0, user);
-                    } finally {
-                        Binder.restoreCallingIdentity(identity);
-                    }
-                }
-
-                isRevokedCompat = mode != AppOpsManager.MODE_ALLOWED
-                        && mode != AppOpsManager.MODE_FOREGROUND;
-            } else {
-                isRevokedCompat = mode != AppOpsManager.MODE_ALLOWED;
-            }
-
-            if (isRevokedCompat && supportsRuntimePermissions) {
-                Slog.w(TAG, "setUidMode() called with a mode inconsistent with runtime"
-                        + " permission state, this is discouraged and you should revoke the"
-                        + " runtime permission instead: uid=" + uid + ", switchCode="
-                        + switchCode + ", mode=" + mode + ", permission=" + permissionName);
-            }
-
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                packageManager.updatePermissionFlags(permissionName, packageName,
-                        PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, isRevokedCompat
-                                ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0, user);
-            } finally {
-                Binder.restoreCallingIdentity(identity);
-            }
-        }
-    }
-
-    private void notifyOpChangedSync(int code, int uid, @NonNull String packageName, int mode,
-            int previousMode) {
-        final StorageManagerInternal storageManagerInternal =
-                LocalServices.getService(StorageManagerInternal.class);
-        if (storageManagerInternal != null) {
-            storageManagerInternal.onAppOpsChanged(code, uid, packageName, mode, previousMode);
-        }
-    }
-
-    @Override
-    public void setMode(int code, int uid, @NonNull String packageName, int mode,
-            @Nullable IAppOpsCallback permissionPolicyCallback) {
-        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid);
-        verifyIncomingOp(code);
-        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
-            return;
-        }
-
-        ArraySet<OnOpModeChangedListener> repCbs = null;
-        code = AppOpsManager.opToSwitch(code);
-
-        PackageVerificationResult pvr;
-        try {
-            pvr = verifyAndGetBypass(uid, packageName, null);
-        } catch (SecurityException e) {
-            Slog.e(TAG, "Cannot setMode", e);
-            return;
-        }
-
-        int previousMode = MODE_DEFAULT;
-        synchronized (this) {
-            UidState uidState = getUidStateLocked(uid, false);
-            Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ true);
-            if (op != null) {
-                if (op.getMode() != mode) {
-                    previousMode = op.getMode();
-                    op.setMode(mode);
-
-                    if (uidState != null) {
-                        uidState.evalForegroundOps();
-                    }
-                    ArraySet<OnOpModeChangedListener> cbs =
-                            mAppOpsServiceInterface.getOpModeChangedListeners(code);
-                    if (cbs != null) {
-                        if (repCbs == null) {
-                            repCbs = new ArraySet<>();
-                        }
-                        repCbs.addAll(cbs);
-                    }
-                    cbs = mAppOpsServiceInterface.getPackageModeChangedListeners(packageName);
-                    if (cbs != null) {
-                        if (repCbs == null) {
-                            repCbs = new ArraySet<>();
-                        }
-                        repCbs.addAll(cbs);
-                    }
-                    if (repCbs != null && permissionPolicyCallback != null) {
-                        repCbs.remove(mModeWatchers.get(permissionPolicyCallback.asBinder()));
-                    }
-                    if (mode == AppOpsManager.opToDefaultMode(op.op)) {
-                        // If going into the default mode, prune this op
-                        // if there is nothing else interesting in it.
-                        pruneOpLocked(op, uid, packageName);
-                    }
-                    scheduleFastWriteLocked();
-                    if (mode != MODE_ERRORED) {
-                        updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
-                    }
-                }
-            }
-        }
-        if (repCbs != null) {
-            mHandler.sendMessage(PooledLambda.obtainMessage(
-                    AppOpsServiceImpl::notifyOpChanged,
-                    this, repCbs, code, uid, packageName));
-        }
-
-        notifyOpChangedSync(code, uid, packageName, mode, previousMode);
-    }
-
-    private void notifyOpChanged(ArraySet<OnOpModeChangedListener> callbacks, int code,
-            int uid, String packageName) {
-        for (int i = 0; i < callbacks.size(); i++) {
-            final OnOpModeChangedListener callback = callbacks.valueAt(i);
-            notifyOpChanged(callback, code, uid, packageName);
-        }
-    }
-
-    private void notifyOpChanged(OnOpModeChangedListener callback, int code,
-            int uid, String packageName) {
-        mAppOpsServiceInterface.notifyOpChanged(callback, code, uid, packageName);
-    }
-
-    private static ArrayList<ChangeRec> addChange(ArrayList<ChangeRec> reports,
-            int op, int uid, String packageName, int previousMode) {
-        boolean duplicate = false;
-        if (reports == null) {
-            reports = new ArrayList<>();
-        } else {
-            final int reportCount = reports.size();
-            for (int j = 0; j < reportCount; j++) {
-                ChangeRec report = reports.get(j);
-                if (report.op == op && report.pkg.equals(packageName)) {
-                    duplicate = true;
-                    break;
-                }
-            }
-        }
-        if (!duplicate) {
-            reports.add(new ChangeRec(op, uid, packageName, previousMode));
-        }
-
-        return reports;
-    }
-
-    private static HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> addCallbacks(
-            HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks,
-            int op, int uid, String packageName, int previousMode,
-            ArraySet<OnOpModeChangedListener> cbs) {
-        if (cbs == null) {
-            return callbacks;
-        }
-        if (callbacks == null) {
-            callbacks = new HashMap<>();
-        }
-        final int N = cbs.size();
-        for (int i=0; i<N; i++) {
-            OnOpModeChangedListener cb = cbs.valueAt(i);
-            ArrayList<ChangeRec> reports = callbacks.get(cb);
-            ArrayList<ChangeRec> changed = addChange(reports, op, uid, packageName, previousMode);
-            if (changed != reports) {
-                callbacks.put(cb, changed);
-            }
-        }
-        return callbacks;
-    }
-
-    static final class ChangeRec {
-        final int op;
-        final int uid;
-        final String pkg;
-        final int previous_mode;
-
-        ChangeRec(int _op, int _uid, String _pkg, int _previous_mode) {
-            op = _op;
-            uid = _uid;
-            pkg = _pkg;
-            previous_mode = _previous_mode;
-        }
-    }
-
-    @Override
-    public void resetAllModes(int reqUserId, String reqPackageName) {
-        final int callingPid = Binder.getCallingPid();
-        final int callingUid = Binder.getCallingUid();
-        reqUserId = ActivityManager.handleIncomingUser(callingPid, callingUid, reqUserId,
-                true, true, "resetAllModes", null);
-
-        int reqUid = -1;
-        if (reqPackageName != null) {
-            try {
-                reqUid = AppGlobals.getPackageManager().getPackageUid(
-                        reqPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, reqUserId);
-            } catch (RemoteException e) {
-                /* ignore - local call */
-            }
-        }
-
-        enforceManageAppOpsModes(callingPid, callingUid, reqUid);
-
-        HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks = null;
-        ArrayList<ChangeRec> allChanges = new ArrayList<>();
-        synchronized (this) {
-            boolean changed = false;
-            for (int i = mUidStates.size() - 1; i >= 0; i--) {
-                UidState uidState = mUidStates.valueAt(i);
-
-                SparseIntArray opModes = uidState.getNonDefaultUidModes();
-                if (opModes != null && (uidState.uid == reqUid || reqUid == -1)) {
-                    final int uidOpCount = opModes.size();
-                    for (int j = uidOpCount - 1; j >= 0; j--) {
-                        final int code = opModes.keyAt(j);
-                        if (AppOpsManager.opAllowsReset(code)) {
-                            int previousMode = opModes.valueAt(j);
-                            uidState.setUidMode(code, AppOpsManager.opToDefaultMode(code));
-                            for (String packageName : getPackagesForUid(uidState.uid)) {
-                                callbacks = addCallbacks(callbacks, code, uidState.uid,
-                                        packageName, previousMode,
-                                        mAppOpsServiceInterface.getOpModeChangedListeners(code));
-                                callbacks = addCallbacks(callbacks, code, uidState.uid,
-                                        packageName, previousMode, mAppOpsServiceInterface
-                                                .getPackageModeChangedListeners(packageName));
-
-                                allChanges = addChange(allChanges, code, uidState.uid,
-                                        packageName, previousMode);
-                            }
-                        }
-                    }
-                }
-
-                if (uidState.pkgOps == null) {
-                    continue;
-                }
-
-                if (reqUserId != UserHandle.USER_ALL
-                        && reqUserId != UserHandle.getUserId(uidState.uid)) {
-                    // Skip any ops for a different user
-                    continue;
-                }
-
-                Map<String, Ops> packages = uidState.pkgOps;
-                Iterator<Map.Entry<String, Ops>> it = packages.entrySet().iterator();
-                boolean uidChanged = false;
-                while (it.hasNext()) {
-                    Map.Entry<String, Ops> ent = it.next();
-                    String packageName = ent.getKey();
-                    if (reqPackageName != null && !reqPackageName.equals(packageName)) {
-                        // Skip any ops for a different package
-                        continue;
-                    }
-                    Ops pkgOps = ent.getValue();
-                    for (int j=pkgOps.size()-1; j>=0; j--) {
-                        Op curOp = pkgOps.valueAt(j);
-                        if (shouldDeferResetOpToDpm(curOp.op)) {
-                            deferResetOpToDpm(curOp.op, reqPackageName, reqUserId);
-                            continue;
-                        }
-                        if (AppOpsManager.opAllowsReset(curOp.op)
-                                && curOp.getMode() != AppOpsManager.opToDefaultMode(curOp.op)) {
-                            int previousMode = curOp.getMode();
-                            curOp.setMode(AppOpsManager.opToDefaultMode(curOp.op));
-                            changed = true;
-                            uidChanged = true;
-                            final int uid = curOp.uidState.uid;
-                            callbacks = addCallbacks(callbacks, curOp.op, uid, packageName,
-                                    previousMode,
-                                    mAppOpsServiceInterface.getOpModeChangedListeners(curOp.op));
-                            callbacks = addCallbacks(callbacks, curOp.op, uid, packageName,
-                                    previousMode, mAppOpsServiceInterface
-                                            .getPackageModeChangedListeners(packageName));
-
-                            allChanges = addChange(allChanges, curOp.op, uid, packageName,
-                                    previousMode);
-                            curOp.removeAttributionsWithNoTime();
-                            if (curOp.mAttributions.isEmpty()) {
-                                pkgOps.removeAt(j);
-                            }
-                        }
-                    }
-                    if (pkgOps.size() == 0) {
-                        it.remove();
-                        mAppOpsServiceInterface.removePackage(packageName,
-                                UserHandle.getUserId(uidState.uid));
-                    }
-                }
-                if (uidState.isDefault()) {
-                    uidState.clear();
-                    mUidStates.remove(uidState.uid);
-                }
-                if (uidChanged) {
-                    uidState.evalForegroundOps();
-                }
-            }
-
-            if (changed) {
-                scheduleFastWriteLocked();
-            }
-        }
-        if (callbacks != null) {
-            for (Map.Entry<OnOpModeChangedListener, ArrayList<ChangeRec>> ent
-                    : callbacks.entrySet()) {
-                OnOpModeChangedListener cb = ent.getKey();
-                ArrayList<ChangeRec> reports = ent.getValue();
-                for (int i=0; i<reports.size(); i++) {
-                    ChangeRec rep = reports.get(i);
-                    mHandler.sendMessage(PooledLambda.obtainMessage(
-                            AppOpsServiceImpl::notifyOpChanged,
-                            this, cb, rep.op, rep.uid, rep.pkg));
-                }
-            }
-        }
-
-        int numChanges = allChanges.size();
-        for (int i = 0; i < numChanges; i++) {
-            ChangeRec change = allChanges.get(i);
-            notifyOpChangedSync(change.op, change.uid, change.pkg,
-                    AppOpsManager.opToDefaultMode(change.op), change.previous_mode);
-        }
-    }
-
-    private boolean shouldDeferResetOpToDpm(int op) {
-        // TODO(b/174582385): avoid special-casing app-op resets by migrating app-op permission
-        //  pre-grants to a role-based mechanism or another general-purpose mechanism.
-        return dpmi != null && dpmi.supportsResetOp(op);
-    }
-
-    /** Assumes {@link #shouldDeferResetOpToDpm(int)} is true. */
-    private void deferResetOpToDpm(int op, String packageName, @UserIdInt int userId) {
-        // TODO(b/174582385): avoid special-casing app-op resets by migrating app-op permission
-        //  pre-grants to a role-based mechanism or another general-purpose mechanism.
-        dpmi.resetOp(op, packageName, userId);
-    }
-
-    private void evalAllForegroundOpsLocked() {
-        for (int uidi = mUidStates.size() - 1; uidi >= 0; uidi--) {
-            final UidState uidState = mUidStates.valueAt(uidi);
-            if (uidState.foregroundOps != null) {
-                uidState.evalForegroundOps();
-            }
-        }
-    }
-
-    @Override
-    public void startWatchingModeWithFlags(int op, String packageName, int flags,
-            IAppOpsCallback callback) {
-        int watchedUid = -1;
-        final int callingUid = Binder.getCallingUid();
-        final int callingPid = Binder.getCallingPid();
-        // TODO: should have a privileged permission to protect this.
-        // Also, if the caller has requested WATCH_FOREGROUND_CHANGES, should we require
-        // the USAGE_STATS permission since this can provide information about when an
-        // app is in the foreground?
-        Preconditions.checkArgumentInRange(op, AppOpsManager.OP_NONE,
-                AppOpsManager._NUM_OP - 1, "Invalid op code: " + op);
-        if (callback == null) {
-            return;
-        }
-        final boolean mayWatchPackageName = packageName != null
-                && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(callingUid));
-        synchronized (this) {
-            int switchOp = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op;
-
-            int notifiedOps;
-            if ((flags & CALL_BACK_ON_SWITCHED_OP) == 0) {
-                if (op == OP_NONE) {
-                    notifiedOps = ALL_OPS;
-                } else {
-                    notifiedOps = op;
-                }
-            } else {
-                notifiedOps = switchOp;
-            }
-
-            ModeCallback cb = mModeWatchers.get(callback.asBinder());
-            if (cb == null) {
-                cb = new ModeCallback(callback, watchedUid, flags, notifiedOps, callingUid,
-                        callingPid);
-                mModeWatchers.put(callback.asBinder(), cb);
-            }
-            if (switchOp != AppOpsManager.OP_NONE) {
-                mAppOpsServiceInterface.startWatchingOpModeChanged(cb, switchOp);
-            }
-            if (mayWatchPackageName) {
-                mAppOpsServiceInterface.startWatchingPackageModeChanged(cb, packageName);
-            }
-            evalAllForegroundOpsLocked();
-        }
-    }
-
-    @Override
-    public void stopWatchingMode(IAppOpsCallback callback) {
-        if (callback == null) {
-            return;
-        }
-        synchronized (this) {
-            ModeCallback cb = mModeWatchers.remove(callback.asBinder());
-            if (cb != null) {
-                cb.unlinkToDeath();
-                mAppOpsServiceInterface.removeListener(cb);
-            }
-
-            evalAllForegroundOpsLocked();
-        }
-    }
-
-    @Override
-    public int checkOperation(int code, int uid, String packageName,
-            @Nullable String attributionTag, boolean raw) {
-        verifyIncomingOp(code);
-        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
-            return AppOpsManager.opToDefaultMode(code);
-        }
-
-        String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
-        if (resolvedPackageName == null) {
-            return AppOpsManager.MODE_IGNORED;
-        }
-        return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag, raw);
-    }
-
-    /**
-     * Get the mode of an app-op.
-     *
-     * @param code        The code of the op
-     * @param uid         The uid of the package the op belongs to
-     * @param packageName The package the op belongs to
-     * @param raw         If the raw state of eval-ed state should be checked.
-     * @return The mode of the op
-     */
-    private @Mode int checkOperationUnchecked(int code, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, boolean raw) {
-        PackageVerificationResult pvr;
-        try {
-            pvr = verifyAndGetBypass(uid, packageName, null);
-        } catch (SecurityException e) {
-            Slog.e(TAG, "checkOperation", e);
-            return AppOpsManager.opToDefaultMode(code);
-        }
-
-        if (isOpRestrictedDueToSuspend(code, packageName, uid)) {
-            return AppOpsManager.MODE_IGNORED;
-        }
-        synchronized (this) {
-            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, true)) {
-                return AppOpsManager.MODE_IGNORED;
-            }
-            code = AppOpsManager.opToSwitch(code);
-            UidState uidState = getUidStateLocked(uid, false);
-            if (uidState != null
-                    && uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) {
-                final int rawMode = uidState.getUidMode(code);
-                return raw ? rawMode : uidState.evalMode(code, rawMode);
-            }
-            Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false);
-            if (op == null) {
-                return AppOpsManager.opToDefaultMode(code);
-            }
-            return raw ? op.getMode() : op.uidState.evalMode(op.op, op.getMode());
-        }
-    }
-
-    @Override
-    public int checkPackage(int uid, String packageName) {
-        Objects.requireNonNull(packageName);
-        try {
-            verifyAndGetBypass(uid, packageName, null);
-            // When the caller is the system, it's possible that the packageName is the special
-            // one (e.g., "root") which isn't actually existed.
-            if (resolveUid(packageName) == uid
-                    || (isPackageExisted(packageName)
-                            && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(uid)))) {
-                return AppOpsManager.MODE_ALLOWED;
-            }
-            return AppOpsManager.MODE_ERRORED;
-        } catch (SecurityException ignored) {
-            return AppOpsManager.MODE_ERRORED;
-        }
-    }
-
-    private boolean isPackageExisted(String packageName) {
-        return getPackageManagerInternal().getPackageStateInternal(packageName) != null;
-    }
-
-    /**
-     * This method will check with PackageManager to determine if the package provided should
-     * be visible to the {@link Binder#getCallingUid()}.
-     *
-     * NOTE: This must not be called while synchronized on {@code this} to avoid dead locks
-     */
-    private boolean filterAppAccessUnlocked(String packageName, int userId) {
-        final int callingUid = Binder.getCallingUid();
-        return LocalServices.getService(PackageManagerInternal.class)
-                .filterAppAccess(packageName, callingUid, userId);
-    }
-
-    @Override
-    public int noteOperation(int code, int uid, @Nullable String packageName,
-            @Nullable String attributionTag, @Nullable String message) {
-        verifyIncomingUid(uid);
-        verifyIncomingOp(code);
-        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
-            return AppOpsManager.MODE_ERRORED;
-        }
-
-        String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
-        if (resolvedPackageName == null) {
-            return AppOpsManager.MODE_IGNORED;
-        }
-        return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
-                Process.INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
-    }
-
-    @Override
-    public int noteOperationUnchecked(int code, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, int proxyUid, String proxyPackageName,
-            @Nullable String proxyAttributionTag, @OpFlags int flags) {
-        PackageVerificationResult pvr;
-        try {
-            pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
-            if (!pvr.isAttributionTagValid) {
-                attributionTag = null;
-            }
-        } catch (SecurityException e) {
-            Slog.e(TAG, "noteOperation", e);
-            return AppOpsManager.MODE_ERRORED;
-        }
-
-        synchronized (this) {
-            final Ops ops = getOpsLocked(uid, packageName, attributionTag,
-                    pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
-            if (ops == null) {
-                scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                        AppOpsManager.MODE_IGNORED);
-                if (DEBUG) {
-                    Slog.d(TAG, "noteOperation: no op for code " + code + " uid " + uid
-                            + " package " + packageName + "flags: "
-                            + AppOpsManager.flagsToString(flags));
-                }
-                return AppOpsManager.MODE_ERRORED;
-            }
-            final Op op = getOpLocked(ops, code, uid, true);
-            final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
-            if (attributedOp.isRunning()) {
-                Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + " code "
-                        + code + " startTime of in progress event="
-                        + attributedOp.mInProgressEvents.valueAt(0).getStartTime());
-            }
-
-            final int switchCode = AppOpsManager.opToSwitch(code);
-            final UidState uidState = ops.uidState;
-            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, false)) {
-                attributedOp.rejected(uidState.getState(), flags);
-                scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                        AppOpsManager.MODE_IGNORED);
-                return AppOpsManager.MODE_IGNORED;
-            }
-            // If there is a non-default per UID policy (we set UID op mode only if
-            // non-default) it takes over, otherwise use the per package policy.
-            if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) {
-                final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode));
-                if (uidMode != AppOpsManager.MODE_ALLOWED) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code "
-                                + switchCode + " (" + code + ") uid " + uid + " package "
-                                + packageName + " flags: " + AppOpsManager.flagsToString(flags));
-                    }
-                    attributedOp.rejected(uidState.getState(), flags);
-                    scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                            uidMode);
-                    return uidMode;
-                }
-            } else {
-                final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
-                        : op;
-                final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode());
-                if (mode != AppOpsManager.MODE_ALLOWED) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "noteOperation: reject #" + mode + " for code "
-                                + switchCode + " (" + code + ") uid " + uid + " package "
-                                + packageName + " flags: " + AppOpsManager.flagsToString(flags));
-                    }
-                    attributedOp.rejected(uidState.getState(), flags);
-                    scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                            mode);
-                    return mode;
-                }
-            }
-            if (DEBUG) {
-                Slog.d(TAG,
-                        "noteOperation: allowing code " + code + " uid " + uid + " package "
-                                + packageName + (attributionTag == null ? ""
-                                : "." + attributionTag) + " flags: "
-                                + AppOpsManager.flagsToString(flags));
-            }
-            scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                    AppOpsManager.MODE_ALLOWED);
-            attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag,
-                    uidState.getState(),
-                    flags);
-
-            return AppOpsManager.MODE_ALLOWED;
-        }
-    }
-
-    @Override
-    public boolean isAttributionTagValid(int uid, @NonNull String packageName,
-            @Nullable String attributionTag,
-            @Nullable String proxyPackageName) {
-        try {
-            return verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName)
-                    .isAttributionTagValid;
-        } catch (SecurityException ignored) {
-            // We don't want to throw, this exception will be handled in the (c/n/s)Operation calls
-            // when they need the bypass object.
-            return false;
-        }
-    }
-
-    // TODO moltmann: Allow watching for attribution ops
-    @Override
-    public void startWatchingActive(int[] ops, IAppOpsActiveCallback callback) {
-        int watchedUid = Process.INVALID_UID;
-        final int callingUid = Binder.getCallingUid();
-        final int callingPid = Binder.getCallingPid();
-        if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
-                != PackageManager.PERMISSION_GRANTED) {
-            watchedUid = callingUid;
-        }
-        if (ops != null) {
-            Preconditions.checkArrayElementsInRange(ops, 0,
-                    AppOpsManager._NUM_OP - 1, "Invalid op code in: " + Arrays.toString(ops));
-        }
-        if (callback == null) {
-            return;
-        }
-        synchronized (this) {
-            SparseArray<ActiveCallback> callbacks = mActiveWatchers.get(callback.asBinder());
-            if (callbacks == null) {
-                callbacks = new SparseArray<>();
-                mActiveWatchers.put(callback.asBinder(), callbacks);
-            }
-            final ActiveCallback activeCallback = new ActiveCallback(callback, watchedUid,
-                    callingUid, callingPid);
-            for (int op : ops) {
-                callbacks.put(op, activeCallback);
-            }
-        }
-    }
-
-    @Override
-    public void stopWatchingActive(IAppOpsActiveCallback callback) {
-        if (callback == null) {
-            return;
-        }
-        synchronized (this) {
-            final SparseArray<ActiveCallback> activeCallbacks =
-                    mActiveWatchers.remove(callback.asBinder());
-            if (activeCallbacks == null) {
-                return;
-            }
-            final int callbackCount = activeCallbacks.size();
-            for (int i = 0; i < callbackCount; i++) {
-                activeCallbacks.valueAt(i).destroy();
-            }
-        }
-    }
-
-    @Override
-    public void startWatchingStarted(int[] ops, @NonNull IAppOpsStartedCallback callback) {
-        int watchedUid = Process.INVALID_UID;
-        final int callingUid = Binder.getCallingUid();
-        final int callingPid = Binder.getCallingPid();
-        if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
-                != PackageManager.PERMISSION_GRANTED) {
-            watchedUid = callingUid;
-        }
-
-        Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty");
-        Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1,
-                "Invalid op code in: " + Arrays.toString(ops));
-        Objects.requireNonNull(callback, "Callback cannot be null");
-
-        synchronized (this) {
-            SparseArray<StartedCallback> callbacks = mStartedWatchers.get(callback.asBinder());
-            if (callbacks == null) {
-                callbacks = new SparseArray<>();
-                mStartedWatchers.put(callback.asBinder(), callbacks);
-            }
-
-            final StartedCallback startedCallback = new StartedCallback(callback, watchedUid,
-                    callingUid, callingPid);
-            for (int op : ops) {
-                callbacks.put(op, startedCallback);
-            }
-        }
-    }
-
-    @Override
-    public void stopWatchingStarted(IAppOpsStartedCallback callback) {
-        Objects.requireNonNull(callback, "Callback cannot be null");
-
-        synchronized (this) {
-            final SparseArray<StartedCallback> startedCallbacks =
-                    mStartedWatchers.remove(callback.asBinder());
-            if (startedCallbacks == null) {
-                return;
-            }
-
-            final int callbackCount = startedCallbacks.size();
-            for (int i = 0; i < callbackCount; i++) {
-                startedCallbacks.valueAt(i).destroy();
-            }
-        }
-    }
-
-    @Override
-    public void startWatchingNoted(@NonNull int[] ops, @NonNull IAppOpsNotedCallback callback) {
-        int watchedUid = Process.INVALID_UID;
-        final int callingUid = Binder.getCallingUid();
-        final int callingPid = Binder.getCallingPid();
-        if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
-                != PackageManager.PERMISSION_GRANTED) {
-            watchedUid = callingUid;
-        }
-        Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty");
-        Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1,
-                "Invalid op code in: " + Arrays.toString(ops));
-        Objects.requireNonNull(callback, "Callback cannot be null");
-        synchronized (this) {
-            SparseArray<NotedCallback> callbacks = mNotedWatchers.get(callback.asBinder());
-            if (callbacks == null) {
-                callbacks = new SparseArray<>();
-                mNotedWatchers.put(callback.asBinder(), callbacks);
-            }
-            final NotedCallback notedCallback = new NotedCallback(callback, watchedUid,
-                    callingUid, callingPid);
-            for (int op : ops) {
-                callbacks.put(op, notedCallback);
-            }
-        }
-    }
-
-    @Override
-    public void stopWatchingNoted(IAppOpsNotedCallback callback) {
-        Objects.requireNonNull(callback, "Callback cannot be null");
-        synchronized (this) {
-            final SparseArray<NotedCallback> notedCallbacks =
-                    mNotedWatchers.remove(callback.asBinder());
-            if (notedCallbacks == null) {
-                return;
-            }
-            final int callbackCount = notedCallbacks.size();
-            for (int i = 0; i < callbackCount; i++) {
-                notedCallbacks.valueAt(i).destroy();
-            }
-        }
-    }
-
-    @Override
-    public int startOperation(@NonNull IBinder clientId, int code, int uid,
-            @Nullable String packageName, @Nullable String attributionTag,
-            boolean startIfModeDefault, @NonNull String message,
-            @AttributionFlags int attributionFlags, int attributionChainId) {
-        verifyIncomingUid(uid);
-        verifyIncomingOp(code);
-        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
-            return AppOpsManager.MODE_ERRORED;
-        }
-
-        String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
-        if (resolvedPackageName == null) {
-            return AppOpsManager.MODE_IGNORED;
-        }
-
-        // As a special case for OP_RECORD_AUDIO_HOTWORD, which we use only for attribution
-        // purposes and not as a check, also make sure that the caller is allowed to access
-        // the data gated by OP_RECORD_AUDIO.
-        //
-        // TODO: Revert this change before Android 12.
-        if (code == OP_RECORD_AUDIO_HOTWORD || code == OP_RECEIVE_AMBIENT_TRIGGER_AUDIO) {
-            int result = checkOperation(OP_RECORD_AUDIO, uid, packageName, null, false);
-            if (result != AppOpsManager.MODE_ALLOWED) {
-                return result;
-            }
-        }
-        return startOperationUnchecked(clientId, code, uid, packageName, attributionTag,
-                Process.INVALID_UID, null, null, OP_FLAG_SELF, startIfModeDefault,
-                attributionFlags, attributionChainId, /*dryRun*/ false);
-    }
-
-    private boolean shouldStartForMode(int mode, boolean startIfModeDefault) {
-        return (mode == MODE_ALLOWED || (mode == MODE_DEFAULT && startIfModeDefault));
-    }
-
-    @Override
-    public int startOperationUnchecked(IBinder clientId, int code, int uid,
-            @NonNull String packageName, @Nullable String attributionTag, int proxyUid,
-            String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags,
-            boolean startIfModeDefault, @AttributionFlags int attributionFlags,
-            int attributionChainId, boolean dryRun) {
-        PackageVerificationResult pvr;
-        try {
-            pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
-            if (!pvr.isAttributionTagValid) {
-                attributionTag = null;
-            }
-        } catch (SecurityException e) {
-            Slog.e(TAG, "startOperation", e);
-            return AppOpsManager.MODE_ERRORED;
-        }
-
-        boolean isRestricted;
-        int startType = START_TYPE_FAILED;
-        synchronized (this) {
-            final Ops ops = getOpsLocked(uid, packageName, attributionTag,
-                    pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
-            if (ops == null) {
-                if (!dryRun) {
-                    scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
-                            flags, AppOpsManager.MODE_IGNORED, startType, attributionFlags,
-                            attributionChainId);
-                }
-                if (DEBUG) {
-                    Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid
-                            + " package " + packageName + " flags: "
-                            + AppOpsManager.flagsToString(flags));
-                }
-                return AppOpsManager.MODE_ERRORED;
-            }
-            final Op op = getOpLocked(ops, code, uid, true);
-            final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
-            final UidState uidState = ops.uidState;
-            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass,
-                    false);
-            final int switchCode = AppOpsManager.opToSwitch(code);
-            // If there is a non-default per UID policy (we set UID op mode only if
-            // non-default) it takes over, otherwise use the per package policy.
-            if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) {
-                final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode));
-                if (!shouldStartForMode(uidMode, startIfModeDefault)) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code "
-                                + switchCode + " (" + code + ") uid " + uid + " package "
-                                + packageName + " flags: " + AppOpsManager.flagsToString(flags));
-                    }
-                    if (!dryRun) {
-                        attributedOp.rejected(uidState.getState(), flags);
-                        scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
-                                flags, uidMode, startType, attributionFlags, attributionChainId);
-                    }
-                    return uidMode;
-                }
-            } else {
-                final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
-                        : op;
-                final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode());
-                if (!shouldStartForMode(mode, startIfModeDefault)) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "startOperation: reject #" + mode + " for code "
-                                + switchCode + " (" + code + ") uid " + uid + " package "
-                                + packageName + " flags: " + AppOpsManager.flagsToString(flags));
-                    }
-                    if (!dryRun) {
-                        attributedOp.rejected(uidState.getState(), flags);
-                        scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
-                                flags, mode, startType, attributionFlags, attributionChainId);
-                    }
-                    return mode;
-                }
-            }
-            if (DEBUG) {
-                Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid
-                        + " package " + packageName + " restricted: " + isRestricted
-                        + " flags: " + AppOpsManager.flagsToString(flags));
-            }
-            if (!dryRun) {
-                try {
-                    if (isRestricted) {
-                        attributedOp.createPaused(clientId, proxyUid, proxyPackageName,
-                                proxyAttributionTag, uidState.getState(), flags,
-                                attributionFlags, attributionChainId);
-                    } else {
-                        attributedOp.started(clientId, proxyUid, proxyPackageName,
-                                proxyAttributionTag, uidState.getState(), flags,
-                                attributionFlags, attributionChainId);
-                        startType = START_TYPE_STARTED;
-                    }
-                } catch (RemoteException e) {
-                    throw new RuntimeException(e);
-                }
-                scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, flags,
-                        isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags,
-                        attributionChainId);
-            }
-        }
-
-        // Possible bug? The raw mode could have been MODE_DEFAULT to reach here.
-        return isRestricted ? MODE_IGNORED : MODE_ALLOWED;
-    }
-
-    @Override
-    public void finishOperation(IBinder clientId, int code, int uid, String packageName,
-            String attributionTag) {
-        verifyIncomingUid(uid);
-        verifyIncomingOp(code);
-        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
-            return;
-        }
-
-        String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
-        if (resolvedPackageName == null) {
-            return;
-        }
-
-        finishOperationUnchecked(clientId, code, uid, resolvedPackageName, attributionTag);
-    }
-
-    @Override
-    public void finishOperationUnchecked(IBinder clientId, int code, int uid, String packageName,
-            String attributionTag) {
-        PackageVerificationResult pvr;
-        try {
-            pvr = verifyAndGetBypass(uid, packageName, attributionTag);
-            if (!pvr.isAttributionTagValid) {
-                attributionTag = null;
-            }
-        } catch (SecurityException e) {
-            Slog.e(TAG, "Cannot finishOperation", e);
-            return;
-        }
-
-        synchronized (this) {
-            Op op = getOpLocked(code, uid, packageName, attributionTag, pvr.isAttributionTagValid,
-                    pvr.bypass, /* edit */ true);
-            if (op == null) {
-                Slog.e(TAG, "Operation not found: uid=" + uid + " pkg=" + packageName + "("
-                        + attributionTag + ") op=" + AppOpsManager.opToName(code));
-                return;
-            }
-            final AttributedOp attributedOp = op.mAttributions.get(attributionTag);
-            if (attributedOp == null) {
-                Slog.e(TAG, "Attribution not found: uid=" + uid + " pkg=" + packageName + "("
-                        + attributionTag + ") op=" + AppOpsManager.opToName(code));
-                return;
-            }
-
-            if (attributedOp.isRunning() || attributedOp.isPaused()) {
-                attributedOp.finished(clientId);
-            } else {
-                Slog.e(TAG, "Operation not started: uid=" + uid + " pkg=" + packageName + "("
-                        + attributionTag + ") op=" + AppOpsManager.opToName(code));
-            }
-        }
-    }
-
-    void scheduleOpActiveChangedIfNeededLocked(int code, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, boolean active,
-            @AttributionFlags int attributionFlags, int attributionChainId) {
-        ArraySet<ActiveCallback> dispatchedCallbacks = null;
-        final int callbackListCount = mActiveWatchers.size();
-        for (int i = 0; i < callbackListCount; i++) {
-            final SparseArray<ActiveCallback> callbacks = mActiveWatchers.valueAt(i);
-            ActiveCallback callback = callbacks.get(code);
-            if (callback != null) {
-                if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
-                    continue;
-                }
-                if (dispatchedCallbacks == null) {
-                    dispatchedCallbacks = new ArraySet<>();
-                }
-                dispatchedCallbacks.add(callback);
-            }
-        }
-        if (dispatchedCallbacks == null) {
-            return;
-        }
-        mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsServiceImpl::notifyOpActiveChanged,
-                this, dispatchedCallbacks, code, uid, packageName, attributionTag, active,
-                attributionFlags, attributionChainId));
-    }
-
-    private void notifyOpActiveChanged(ArraySet<ActiveCallback> callbacks,
-            int code, int uid, @NonNull String packageName, @Nullable String attributionTag,
-            boolean active, @AttributionFlags int attributionFlags, int attributionChainId) {
-        // There are features watching for mode changes such as window manager
-        // and location manager which are in our process. The callbacks in these
-        // features may require permissions our remote caller does not have.
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            final int callbackCount = callbacks.size();
-            for (int i = 0; i < callbackCount; i++) {
-                final ActiveCallback callback = callbacks.valueAt(i);
-                try {
-                    if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
-                        continue;
-                    }
-                    callback.mCallback.opActiveChanged(code, uid, packageName, attributionTag,
-                            active, attributionFlags, attributionChainId);
-                } catch (RemoteException e) {
-                    /* do nothing */
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName,
-            String attributionTag, @OpFlags int flags, @Mode int result,
-            @AppOpsManager.OnOpStartedListener.StartedType int startedType,
-            @AttributionFlags int attributionFlags, int attributionChainId) {
-        ArraySet<StartedCallback> dispatchedCallbacks = null;
-        final int callbackListCount = mStartedWatchers.size();
-        for (int i = 0; i < callbackListCount; i++) {
-            final SparseArray<StartedCallback> callbacks = mStartedWatchers.valueAt(i);
-
-            StartedCallback callback = callbacks.get(code);
-            if (callback != null) {
-                if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
-                    continue;
-                }
-
-                if (dispatchedCallbacks == null) {
-                    dispatchedCallbacks = new ArraySet<>();
-                }
-                dispatchedCallbacks.add(callback);
-            }
-        }
-
-        if (dispatchedCallbacks == null) {
-            return;
-        }
-
-        mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsServiceImpl::notifyOpStarted,
-                this, dispatchedCallbacks, code, uid, pkgName, attributionTag, flags,
-                result, startedType, attributionFlags, attributionChainId));
-    }
-
-    private void notifyOpStarted(ArraySet<StartedCallback> callbacks,
-            int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
-            @Mode int result, @AppOpsManager.OnOpStartedListener.StartedType int startedType,
-            @AttributionFlags int attributionFlags, int attributionChainId) {
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            final int callbackCount = callbacks.size();
-            for (int i = 0; i < callbackCount; i++) {
-                final StartedCallback callback = callbacks.valueAt(i);
-                try {
-                    if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
-                        continue;
-                    }
-                    callback.mCallback.opStarted(code, uid, packageName, attributionTag, flags,
-                            result, startedType, attributionFlags, attributionChainId);
-                } catch (RemoteException e) {
-                    /* do nothing */
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    private void scheduleOpNotedIfNeededLocked(int code, int uid, String packageName,
-            String attributionTag, @OpFlags int flags, @Mode int result) {
-        ArraySet<NotedCallback> dispatchedCallbacks = null;
-        final int callbackListCount = mNotedWatchers.size();
-        for (int i = 0; i < callbackListCount; i++) {
-            final SparseArray<NotedCallback> callbacks = mNotedWatchers.valueAt(i);
-            final NotedCallback callback = callbacks.get(code);
-            if (callback != null) {
-                if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
-                    continue;
-                }
-                if (dispatchedCallbacks == null) {
-                    dispatchedCallbacks = new ArraySet<>();
-                }
-                dispatchedCallbacks.add(callback);
-            }
-        }
-        if (dispatchedCallbacks == null) {
-            return;
-        }
-        mHandler.sendMessage(PooledLambda.obtainMessage(
-                AppOpsServiceImpl::notifyOpChecked,
-                this, dispatchedCallbacks, code, uid, packageName, attributionTag, flags,
-                result));
-    }
-
-    private void notifyOpChecked(ArraySet<NotedCallback> callbacks,
-            int code, int uid, String packageName, String attributionTag, @OpFlags int flags,
-            @Mode int result) {
-        // There are features watching for checks in our process. The callbacks in
-        // these features may require permissions our remote caller does not have.
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            final int callbackCount = callbacks.size();
-            for (int i = 0; i < callbackCount; i++) {
-                final NotedCallback callback = callbacks.valueAt(i);
-                try {
-                    if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) {
-                        continue;
-                    }
-                    callback.mCallback.opNoted(code, uid, packageName, attributionTag, flags,
-                            result);
-                } catch (RemoteException e) {
-                    /* do nothing */
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    private void verifyIncomingUid(int uid) {
-        if (uid == Binder.getCallingUid()) {
-            return;
-        }
-        if (Binder.getCallingPid() == Process.myPid()) {
-            return;
-        }
-        mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
-                Binder.getCallingPid(), Binder.getCallingUid(), null);
-    }
-
-    private boolean shouldIgnoreCallback(int op, int watcherPid, int watcherUid) {
-        // If it's a restricted read op, ignore it if watcher doesn't have manage ops permission,
-        // as watcher should not use this to signal if the value is changed.
-        return opRestrictsRead(op) && mContext.checkPermission(Manifest.permission.MANAGE_APPOPS,
-                watcherPid, watcherUid) != PackageManager.PERMISSION_GRANTED;
-    }
-
-    private void verifyIncomingOp(int op) {
-        if (op >= 0 && op < AppOpsManager._NUM_OP) {
-            // Enforce manage appops permission if it's a restricted read op.
-            if (opRestrictsRead(op)) {
-                mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS,
-                        Binder.getCallingPid(), Binder.getCallingUid(), "verifyIncomingOp");
-            }
-            return;
-        }
-        throw new IllegalArgumentException("Bad operation #" + op);
-    }
-
-    private boolean isIncomingPackageValid(@Nullable String packageName, @UserIdInt int userId) {
-        final int callingUid = Binder.getCallingUid();
-        // Handle the special UIDs that don't have actual packages (audioserver, cameraserver, etc).
-        if (packageName == null || isSpecialPackage(callingUid, packageName)) {
-            return true;
-        }
-
-        // If the package doesn't exist, #verifyAndGetBypass would throw a SecurityException in
-        // the end. Although that exception would be caught and return, we could make it return
-        // early.
-        if (!isPackageExisted(packageName)) {
-            return false;
-        }
-
-        if (getPackageManagerInternal().filterAppAccess(packageName, callingUid, userId)) {
-            Slog.w(TAG, packageName + " not found from " + callingUid);
-            return false;
-        }
-
-        return true;
-    }
-
-    private boolean isSpecialPackage(int callingUid, @Nullable String packageName) {
-        final String resolvedPackage = AppOpsManager.resolvePackageName(callingUid, packageName);
-        return callingUid == Process.SYSTEM_UID
-                || resolveUid(resolvedPackage) != Process.INVALID_UID;
-    }
-
-    private @Nullable UidState getUidStateLocked(int uid, boolean edit) {
-        UidState uidState = mUidStates.get(uid);
-        if (uidState == null) {
-            if (!edit) {
-                return null;
-            }
-            uidState = new UidState(uid);
-            mUidStates.put(uid, uidState);
-        }
-
-        return uidState;
-    }
-
-    @Override
-    public void updateAppWidgetVisibility(SparseArray<String> uidPackageNames, boolean visible) {
-        synchronized (this) {
-            getUidStateTracker().updateAppWidgetVisibility(uidPackageNames, visible);
-        }
-    }
-
-    /**
-     * @return {@link PackageManagerInternal}
-     */
-    private @NonNull PackageManagerInternal getPackageManagerInternal() {
-        if (mPackageManagerInternal == null) {
-            mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
-        }
-
-        return mPackageManagerInternal;
-    }
-
-    @Override
-    public void verifyPackage(int uid, String packageName) {
-        verifyAndGetBypass(uid, packageName, null);
-    }
-
-    /**
-     * Create a restriction description matching the properties of the package.
-     *
-     * @param pkg The package to create the restriction description for
-     * @return The restriction matching the package
-     */
-    private RestrictionBypass getBypassforPackage(@NonNull AndroidPackage pkg) {
-        return new RestrictionBypass(pkg.getUid() == Process.SYSTEM_UID, pkg.isPrivileged(),
-                mContext.checkPermission(android.Manifest.permission
-                        .EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS, -1, pkg.getUid())
-                        == PackageManager.PERMISSION_GRANTED);
-    }
-
-    /**
-     * @see #verifyAndGetBypass(int, String, String, String)
-     */
-    private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
-            @Nullable String attributionTag) {
-        return verifyAndGetBypass(uid, packageName, attributionTag, null);
-    }
-
-    /**
-     * Verify that package belongs to uid and return the {@link RestrictionBypass bypass
-     * description} for the package, along with a boolean indicating whether the attribution tag is
-     * valid.
-     *
-     * @param uid              The uid the package belongs to
-     * @param packageName      The package the might belong to the uid
-     * @param attributionTag   attribution tag or {@code null} if no need to verify
-     * @param proxyPackageName The proxy package, from which the attribution tag is to be pulled
-     * @return PackageVerificationResult containing {@link RestrictionBypass} and whether the
-     * attribution tag is valid
-     */
-    private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
-            @Nullable String attributionTag, @Nullable String proxyPackageName) {
-        if (uid == Process.ROOT_UID) {
-            // For backwards compatibility, don't check package name for root UID.
-            return new PackageVerificationResult(null,
-                    /* isAttributionTagValid */ true);
-        }
-        if (Process.isSdkSandboxUid(uid)) {
-            // SDK sandbox processes run in their own UID range, but their associated
-            // UID for checks should always be the UID of the package implementing SDK sandbox
-            // service.
-            // TODO: We will need to modify the callers of this function instead, so
-            // modifications and checks against the app ops state are done with the
-            // correct UID.
-            try {
-                final PackageManager pm = mContext.getPackageManager();
-                final String supplementalPackageName = pm.getSdkSandboxPackageName();
-                if (Objects.equals(packageName, supplementalPackageName)) {
-                    uid = pm.getPackageUidAsUser(supplementalPackageName,
-                            PackageManager.PackageInfoFlags.of(0), UserHandle.getUserId(uid));
-                }
-            } catch (PackageManager.NameNotFoundException e) {
-                // Shouldn't happen for the supplemental package
-                e.printStackTrace();
-            }
-        }
-
-
-        // Do not check if uid/packageName/attributionTag is already known.
-        synchronized (this) {
-            UidState uidState = mUidStates.get(uid);
-            if (uidState != null && uidState.pkgOps != null) {
-                Ops ops = uidState.pkgOps.get(packageName);
-
-                if (ops != null && (attributionTag == null || ops.knownAttributionTags.contains(
-                        attributionTag)) && ops.bypass != null) {
-                    return new PackageVerificationResult(ops.bypass,
-                            ops.validAttributionTags.contains(attributionTag));
-                }
-            }
-        }
-
-        int callingUid = Binder.getCallingUid();
-
-        // Allow any attribution tag for resolvable uids
-        int pkgUid;
-        if (Objects.equals(packageName, "com.android.shell")) {
-            // Special case for the shell which is a package but should be able
-            // to bypass app attribution tag restrictions.
-            pkgUid = Process.SHELL_UID;
-        } else {
-            pkgUid = resolveUid(packageName);
-        }
-        if (pkgUid != Process.INVALID_UID) {
-            if (pkgUid != UserHandle.getAppId(uid)) {
-                Slog.e(TAG, "Bad call made by uid " + callingUid + ". "
-                        + "Package \"" + packageName + "\" does not belong to uid " + uid + ".");
-                String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not";
-                throw new SecurityException("Specified package \"" + packageName + "\" under uid "
-                        + UserHandle.getAppId(uid) + otherUidMessage);
-            }
-            return new PackageVerificationResult(RestrictionBypass.UNRESTRICTED,
-                    /* isAttributionTagValid */ true);
-        }
-
-        int userId = UserHandle.getUserId(uid);
-        RestrictionBypass bypass = null;
-        boolean isAttributionTagValid = false;
-
-        final long ident = Binder.clearCallingIdentity();
-        try {
-            PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class);
-            AndroidPackage pkg = pmInt.getPackage(packageName);
-            if (pkg != null) {
-                isAttributionTagValid = isAttributionInPackage(pkg, attributionTag);
-                pkgUid = UserHandle.getUid(userId, UserHandle.getAppId(pkg.getUid()));
-                bypass = getBypassforPackage(pkg);
-            }
-            if (!isAttributionTagValid) {
-                AndroidPackage proxyPkg = proxyPackageName != null
-                        ? pmInt.getPackage(proxyPackageName) : null;
-                // Re-check in proxy.
-                isAttributionTagValid = isAttributionInPackage(proxyPkg, attributionTag);
-                String msg;
-                if (pkg != null && isAttributionTagValid) {
-                    msg = "attributionTag " + attributionTag + " declared in manifest of the proxy"
-                            + " package " + proxyPackageName + ", this is not advised";
-                } else if (pkg != null) {
-                    msg = "attributionTag " + attributionTag + " not declared in manifest of "
-                            + packageName;
-                } else {
-                    msg = "package " + packageName + " not found, can't check for "
-                            + "attributionTag " + attributionTag;
-                }
-
-                try {
-                    if (!mPlatformCompat.isChangeEnabledByPackageName(
-                            SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE, packageName,
-                            userId) || !mPlatformCompat.isChangeEnabledByUid(
-                            SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE,
-                            callingUid)) {
-                        // Do not override tags if overriding is not enabled for this package
-                        isAttributionTagValid = true;
-                    }
-                    Slog.e(TAG, msg);
-                } catch (RemoteException neverHappens) {
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-
-        if (pkgUid != uid) {
-            Slog.e(TAG, "Bad call made by uid " + callingUid + ". "
-                    + "Package \"" + packageName + "\" does not belong to uid " + uid + ".");
-            String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not";
-            throw new SecurityException("Specified package \"" + packageName + "\" under uid " + uid
-                    + otherUidMessage);
-        }
-
-        return new PackageVerificationResult(bypass, isAttributionTagValid);
-    }
-
-    private boolean isAttributionInPackage(@Nullable AndroidPackage pkg,
-            @Nullable String attributionTag) {
-        if (pkg == null) {
-            return false;
-        } else if (attributionTag == null) {
-            return true;
-        }
-        if (pkg.getAttributions() != null) {
-            int numAttributions = pkg.getAttributions().size();
-            for (int i = 0; i < numAttributions; i++) {
-                if (pkg.getAttributions().get(i).getTag().equals(attributionTag)) {
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Get (and potentially create) ops.
-     *
-     * @param uid                   The uid the package belongs to
-     * @param packageName           The name of the package
-     * @param attributionTag        attribution tag
-     * @param isAttributionTagValid whether the given attribution tag is valid
-     * @param bypass                When to bypass certain op restrictions (can be null if edit
-        *                              == false)
-     * @param edit                  If an ops does not exist, create the ops?
-     * @return The ops
-     */
-    private Ops getOpsLocked(int uid, String packageName, @Nullable String attributionTag,
-            boolean isAttributionTagValid, @Nullable RestrictionBypass bypass, boolean edit) {
-        UidState uidState = getUidStateLocked(uid, edit);
-        if (uidState == null) {
-            return null;
-        }
-
-        if (uidState.pkgOps == null) {
-            if (!edit) {
-                return null;
-            }
-            uidState.pkgOps = new ArrayMap<>();
-        }
-
-        Ops ops = uidState.pkgOps.get(packageName);
-        if (ops == null) {
-            if (!edit) {
-                return null;
-            }
-            ops = new Ops(packageName, uidState);
-            uidState.pkgOps.put(packageName, ops);
-        }
-
-        if (edit) {
-            if (bypass != null) {
-                ops.bypass = bypass;
-            }
-
-            if (attributionTag != null) {
-                ops.knownAttributionTags.add(attributionTag);
-                if (isAttributionTagValid) {
-                    ops.validAttributionTags.add(attributionTag);
-                } else {
-                    ops.validAttributionTags.remove(attributionTag);
-                }
-            }
-        }
-
-        return ops;
-    }
-
-    @Override
-    public void scheduleWriteLocked() {
-        if (!mWriteScheduled) {
-            mWriteScheduled = true;
-            mHandler.postDelayed(mWriteRunner, WRITE_DELAY);
-        }
-    }
-
-    @Override
-    public void scheduleFastWriteLocked() {
-        if (!mFastWriteScheduled) {
-            mWriteScheduled = true;
-            mFastWriteScheduled = true;
-            mHandler.removeCallbacks(mWriteRunner);
-            mHandler.postDelayed(mWriteRunner, 10 * 1000);
-        }
-    }
-
-    /**
-     * Get the state of an op for a uid.
-     *
-     * @param code                  The code of the op
-     * @param uid                   The uid the of the package
-     * @param packageName           The package name for which to get the state for
-     * @param attributionTag        The attribution tag
-     * @param isAttributionTagValid Whether the given attribution tag is valid
-     * @param bypass                When to bypass certain op restrictions (can be null if edit
-     *                              == false)
-     * @param edit                  Iff {@code true} create the {@link Op} object if not yet created
-     * @return The {@link Op state} of the op
-     */
-    private @Nullable Op getOpLocked(int code, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, boolean isAttributionTagValid,
-            @Nullable RestrictionBypass bypass, boolean edit) {
-        Ops ops = getOpsLocked(uid, packageName, attributionTag, isAttributionTagValid, bypass,
-                edit);
-        if (ops == null) {
-            return null;
-        }
-        return getOpLocked(ops, code, uid, edit);
-    }
-
-    private Op getOpLocked(Ops ops, int code, int uid, boolean edit) {
-        Op op = ops.get(code);
-        if (op == null) {
-            if (!edit) {
-                return null;
-            }
-            op = new Op(ops.uidState, ops.packageName, code, uid);
-            ops.put(code, op);
-        }
-        if (edit) {
-            scheduleWriteLocked();
-        }
-        return op;
-    }
-
-    private boolean isOpRestrictedDueToSuspend(int code, String packageName, int uid) {
-        if (!ArrayUtils.contains(OPS_RESTRICTED_ON_SUSPEND, code)) {
-            return false;
-        }
-        final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
-        return pmi.isPackageSuspended(packageName, UserHandle.getUserId(uid));
-    }
-
-    private boolean isOpRestrictedLocked(int uid, int code, String packageName,
-            String attributionTag, @Nullable RestrictionBypass appBypass, boolean isCheckOp) {
-        int restrictionSetCount = mOpGlobalRestrictions.size();
-
-        for (int i = 0; i < restrictionSetCount; i++) {
-            ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.valueAt(i);
-            if (restrictionState.hasRestriction(code)) {
-                return true;
-            }
-        }
-
-        int userHandle = UserHandle.getUserId(uid);
-        restrictionSetCount = mOpUserRestrictions.size();
-
-        for (int i = 0; i < restrictionSetCount; i++) {
-            // For each client, check that the given op is not restricted, or that the given
-            // package is exempt from the restriction.
-            ClientUserRestrictionState restrictionState = mOpUserRestrictions.valueAt(i);
-            if (restrictionState.hasRestriction(code, packageName, attributionTag, userHandle,
-                    isCheckOp)) {
-                RestrictionBypass opBypass = opAllowSystemBypassRestriction(code);
-                if (opBypass != null) {
-                    // If we are the system, bypass user restrictions for certain codes
-                    synchronized (this) {
-                        if (opBypass.isSystemUid && appBypass != null && appBypass.isSystemUid) {
-                            return false;
-                        }
-                        if (opBypass.isPrivileged && appBypass != null && appBypass.isPrivileged) {
-                            return false;
-                        }
-                        if (opBypass.isRecordAudioRestrictionExcept && appBypass != null
-                                && appBypass.isRecordAudioRestrictionExcept) {
-                            return false;
-                        }
-                    }
-                }
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public void readState() {
-        synchronized (mFile) {
-            synchronized (this) {
-                FileInputStream stream;
-                try {
-                    stream = mFile.openRead();
-                } catch (FileNotFoundException e) {
-                    Slog.i(TAG, "No existing app ops " + mFile.getBaseFile() + "; starting empty");
-                    return;
-                }
-                boolean success = false;
-                mUidStates.clear();
-                mAppOpsServiceInterface.clearAllModes();
-                try {
-                    TypedXmlPullParser parser = Xml.resolvePullParser(stream);
-                    int type;
-                    while ((type = parser.next()) != XmlPullParser.START_TAG
-                            && type != XmlPullParser.END_DOCUMENT) {
-                        // Parse next until we reach the start or end
-                    }
-
-                    if (type != XmlPullParser.START_TAG) {
-                        throw new IllegalStateException("no start tag found");
-                    }
-
-                    mVersionAtBoot = parser.getAttributeInt(null, "v", NO_VERSION);
-
-                    int outerDepth = parser.getDepth();
-                    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                            && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-                        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
-                            continue;
-                        }
-
-                        String tagName = parser.getName();
-                        if (tagName.equals("pkg")) {
-                            readPackage(parser);
-                        } else if (tagName.equals("uid")) {
-                            readUidOps(parser);
-                        } else {
-                            Slog.w(TAG, "Unknown element under <app-ops>: "
-                                    + parser.getName());
-                            XmlUtils.skipCurrentTag(parser);
-                        }
-                    }
-                    success = true;
-                } catch (IllegalStateException e) {
-                    Slog.w(TAG, "Failed parsing " + e);
-                } catch (NullPointerException e) {
-                    Slog.w(TAG, "Failed parsing " + e);
-                } catch (NumberFormatException e) {
-                    Slog.w(TAG, "Failed parsing " + e);
-                } catch (XmlPullParserException e) {
-                    Slog.w(TAG, "Failed parsing " + e);
-                } catch (IOException e) {
-                    Slog.w(TAG, "Failed parsing " + e);
-                } catch (IndexOutOfBoundsException e) {
-                    Slog.w(TAG, "Failed parsing " + e);
-                } finally {
-                    if (!success) {
-                        mUidStates.clear();
-                        mAppOpsServiceInterface.clearAllModes();
-                    }
-                    try {
-                        stream.close();
-                    } catch (IOException e) {
-                    }
-                }
-            }
-        }
-    }
-
-    @VisibleForTesting
-    @GuardedBy("this")
-    void upgradeRunAnyInBackgroundLocked() {
-        for (int i = 0; i < mUidStates.size(); i++) {
-            final UidState uidState = mUidStates.valueAt(i);
-            if (uidState == null) {
-                continue;
-            }
-            SparseIntArray opModes = uidState.getNonDefaultUidModes();
-            if (opModes != null) {
-                final int idx = opModes.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND);
-                if (idx >= 0) {
-                    uidState.setUidMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
-                            opModes.valueAt(idx));
-                }
-            }
-            if (uidState.pkgOps == null) {
-                continue;
-            }
-            boolean changed = false;
-            for (int j = 0; j < uidState.pkgOps.size(); j++) {
-                Ops ops = uidState.pkgOps.valueAt(j);
-                if (ops != null) {
-                    final Op op = ops.get(AppOpsManager.OP_RUN_IN_BACKGROUND);
-                    if (op != null && op.getMode() != AppOpsManager.opToDefaultMode(op.op)) {
-                        final Op copy = new Op(op.uidState, op.packageName,
-                                AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uidState.uid);
-                        copy.setMode(op.getMode());
-                        ops.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, copy);
-                        changed = true;
-                    }
-                }
-            }
-            if (changed) {
-                uidState.evalForegroundOps();
-            }
-        }
-    }
-
-    /**
-     * The interpretation of the default mode - MODE_DEFAULT - for OP_SCHEDULE_EXACT_ALARM is
-     * changing. Simultaneously, we want to change this op's mode from MODE_DEFAULT to MODE_ALLOWED
-     * for already installed apps. For newer apps, it will stay as MODE_DEFAULT.
-     */
-    @VisibleForTesting
-    @GuardedBy("this")
-    void upgradeScheduleExactAlarmLocked() {
-        final PermissionManagerServiceInternal pmsi = LocalServices.getService(
-                PermissionManagerServiceInternal.class);
-        final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
-        final PackageManagerInternal pmi = getPackageManagerInternal();
-
-        final String[] packagesDeclaringPermission = pmsi.getAppOpPermissionPackages(
-                AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM));
-        final int[] userIds = umi.getUserIds();
-
-        for (final String pkg : packagesDeclaringPermission) {
-            for (int userId : userIds) {
-                final int uid = pmi.getPackageUid(pkg, 0, userId);
-
-                UidState uidState = mUidStates.get(uid);
-                if (uidState == null) {
-                    uidState = new UidState(uid);
-                    mUidStates.put(uid, uidState);
-                }
-                final int oldMode = uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM);
-                if (oldMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) {
-                    uidState.setUidMode(OP_SCHEDULE_EXACT_ALARM, MODE_ALLOWED);
-                }
-            }
-            // This appop is meant to be controlled at a uid level. So we leave package modes as
-            // they are.
-        }
-    }
-
-    @GuardedBy("this")
-    private void upgradeLocked(int oldVersion) {
-        if (oldVersion == NO_FILE_VERSION || oldVersion >= CURRENT_VERSION) {
-            return;
-        }
-        Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION);
-        switch (oldVersion) {
-            case NO_VERSION:
-                upgradeRunAnyInBackgroundLocked();
-                // fall through
-            case 1:
-                upgradeScheduleExactAlarmLocked();
-                // fall through
-            case 2:
-                // for future upgrades
-        }
-        scheduleFastWriteLocked();
-    }
-
-    private void readUidOps(TypedXmlPullParser parser) throws NumberFormatException,
-            XmlPullParserException, IOException {
-        final int uid = parser.getAttributeInt(null, "n");
-        int outerDepth = parser.getDepth();
-        int type;
-        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
-                continue;
-            }
-
-            String tagName = parser.getName();
-            if (tagName.equals("op")) {
-                final int code = parser.getAttributeInt(null, "n");
-                final int mode = parser.getAttributeInt(null, "m");
-                setUidMode(code, uid, mode, null);
-            } else {
-                Slog.w(TAG, "Unknown element under <uid-ops>: "
-                        + parser.getName());
-                XmlUtils.skipCurrentTag(parser);
-            }
-        }
-    }
-
-    private void readPackage(TypedXmlPullParser parser)
-            throws NumberFormatException, XmlPullParserException, IOException {
-        String pkgName = parser.getAttributeValue(null, "n");
-        int outerDepth = parser.getDepth();
-        int type;
-        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
-                continue;
-            }
-
-            String tagName = parser.getName();
-            if (tagName.equals("uid")) {
-                readUid(parser, pkgName);
-            } else {
-                Slog.w(TAG, "Unknown element under <pkg>: "
-                        + parser.getName());
-                XmlUtils.skipCurrentTag(parser);
-            }
-        }
-    }
-
-    private void readUid(TypedXmlPullParser parser, String pkgName)
-            throws NumberFormatException, XmlPullParserException, IOException {
-        int uid = parser.getAttributeInt(null, "n");
-        final UidState uidState = getUidStateLocked(uid, true);
-        int outerDepth = parser.getDepth();
-        int type;
-        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
-                continue;
-            }
-            String tagName = parser.getName();
-            if (tagName.equals("op")) {
-                readOp(parser, uidState, pkgName);
-            } else {
-                Slog.w(TAG, "Unknown element under <pkg>: "
-                        + parser.getName());
-                XmlUtils.skipCurrentTag(parser);
-            }
-        }
-        uidState.evalForegroundOps();
-    }
-
-    private void readAttributionOp(TypedXmlPullParser parser, @NonNull Op parent,
-            @Nullable String attribution)
-            throws NumberFormatException, IOException, XmlPullParserException {
-        final AttributedOp attributedOp = parent.getOrCreateAttribution(parent, attribution);
-
-        final long key = parser.getAttributeLong(null, "n");
-        final int uidState = extractUidStateFromKey(key);
-        final int opFlags = extractFlagsFromKey(key);
-
-        final long accessTime = parser.getAttributeLong(null, "t", 0);
-        final long rejectTime = parser.getAttributeLong(null, "r", 0);
-        final long accessDuration = parser.getAttributeLong(null, "d", -1);
-        final String proxyPkg = XmlUtils.readStringAttribute(parser, "pp");
-        final int proxyUid = parser.getAttributeInt(null, "pu", Process.INVALID_UID);
-        final String proxyAttributionTag = XmlUtils.readStringAttribute(parser, "pc");
-
-        if (accessTime > 0) {
-            attributedOp.accessed(accessTime, accessDuration, proxyUid, proxyPkg,
-                    proxyAttributionTag, uidState, opFlags);
-        }
-        if (rejectTime > 0) {
-            attributedOp.rejected(rejectTime, uidState, opFlags);
-        }
-    }
-
-    private void readOp(TypedXmlPullParser parser,
-            @NonNull UidState uidState, @NonNull String pkgName)
-            throws NumberFormatException, XmlPullParserException, IOException {
-        int opCode = parser.getAttributeInt(null, "n");
-        Op op = new Op(uidState, pkgName, opCode, uidState.uid);
-
-        final int mode = parser.getAttributeInt(null, "m", AppOpsManager.opToDefaultMode(op.op));
-        op.setMode(mode);
-
-        int outerDepth = parser.getDepth();
-        int type;
-        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
-                continue;
-            }
-            String tagName = parser.getName();
-            if (tagName.equals("st")) {
-                readAttributionOp(parser, op, XmlUtils.readStringAttribute(parser, "id"));
-            } else {
-                Slog.w(TAG, "Unknown element under <op>: "
-                        + parser.getName());
-                XmlUtils.skipCurrentTag(parser);
-            }
-        }
-
-        if (uidState.pkgOps == null) {
-            uidState.pkgOps = new ArrayMap<>();
-        }
-        Ops ops = uidState.pkgOps.get(pkgName);
-        if (ops == null) {
-            ops = new Ops(pkgName, uidState);
-            uidState.pkgOps.put(pkgName, ops);
-        }
-        ops.put(op.op, op);
-    }
-
-    @Override
-    public void writeState() {
-        synchronized (mFile) {
-            FileOutputStream stream;
-            try {
-                stream = mFile.startWrite();
-            } catch (IOException e) {
-                Slog.w(TAG, "Failed to write state: " + e);
-                return;
-            }
-
-            List<AppOpsManager.PackageOps> allOps = getPackagesForOps(null);
-
-            try {
-                TypedXmlSerializer out = Xml.resolveSerializer(stream);
-                out.startDocument(null, true);
-                out.startTag(null, "app-ops");
-                out.attributeInt(null, "v", CURRENT_VERSION);
-
-                SparseArray<SparseIntArray> uidStatesClone;
-                synchronized (this) {
-                    uidStatesClone = new SparseArray<>(mUidStates.size());
-
-                    final int uidStateCount = mUidStates.size();
-                    for (int uidStateNum = 0; uidStateNum < uidStateCount; uidStateNum++) {
-                        UidState uidState = mUidStates.valueAt(uidStateNum);
-                        int uid = mUidStates.keyAt(uidStateNum);
-
-                        SparseIntArray opModes = uidState.getNonDefaultUidModes();
-                        if (opModes != null && opModes.size() > 0) {
-                            uidStatesClone.put(uid, opModes);
-                        }
-                    }
-                }
-
-                final int uidStateCount = uidStatesClone.size();
-                for (int uidStateNum = 0; uidStateNum < uidStateCount; uidStateNum++) {
-                    SparseIntArray opModes = uidStatesClone.valueAt(uidStateNum);
-                    if (opModes != null && opModes.size() > 0) {
-                        out.startTag(null, "uid");
-                        out.attributeInt(null, "n", uidStatesClone.keyAt(uidStateNum));
-                        final int opCount = opModes.size();
-                        for (int opCountNum = 0; opCountNum < opCount; opCountNum++) {
-                            final int op = opModes.keyAt(opCountNum);
-                            final int mode = opModes.valueAt(opCountNum);
-                            out.startTag(null, "op");
-                            out.attributeInt(null, "n", op);
-                            out.attributeInt(null, "m", mode);
-                            out.endTag(null, "op");
-                        }
-                        out.endTag(null, "uid");
-                    }
-                }
-
-                if (allOps != null) {
-                    String lastPkg = null;
-                    for (int i = 0; i < allOps.size(); i++) {
-                        AppOpsManager.PackageOps pkg = allOps.get(i);
-                        if (!Objects.equals(pkg.getPackageName(), lastPkg)) {
-                            if (lastPkg != null) {
-                                out.endTag(null, "pkg");
-                            }
-                            lastPkg = pkg.getPackageName();
-                            if (lastPkg != null) {
-                                out.startTag(null, "pkg");
-                                out.attribute(null, "n", lastPkg);
-                            }
-                        }
-                        out.startTag(null, "uid");
-                        out.attributeInt(null, "n", pkg.getUid());
-                        List<AppOpsManager.OpEntry> ops = pkg.getOps();
-                        for (int j = 0; j < ops.size(); j++) {
-                            AppOpsManager.OpEntry op = ops.get(j);
-                            out.startTag(null, "op");
-                            out.attributeInt(null, "n", op.getOp());
-                            if (op.getMode() != AppOpsManager.opToDefaultMode(op.getOp())) {
-                                out.attributeInt(null, "m", op.getMode());
-                            }
-
-                            for (String attributionTag : op.getAttributedOpEntries().keySet()) {
-                                final AttributedOpEntry attribution =
-                                        op.getAttributedOpEntries().get(attributionTag);
-
-                                final ArraySet<Long> keys = attribution.collectKeys();
-
-                                final int keyCount = keys.size();
-                                for (int k = 0; k < keyCount; k++) {
-                                    final long key = keys.valueAt(k);
-
-                                    final int uidState = AppOpsManager.extractUidStateFromKey(key);
-                                    final int flags = AppOpsManager.extractFlagsFromKey(key);
-
-                                    final long accessTime = attribution.getLastAccessTime(uidState,
-                                            uidState, flags);
-                                    final long rejectTime = attribution.getLastRejectTime(uidState,
-                                            uidState, flags);
-                                    final long accessDuration = attribution.getLastDuration(
-                                            uidState, uidState, flags);
-                                    // Proxy information for rejections is not backed up
-                                    final OpEventProxyInfo proxy = attribution.getLastProxyInfo(
-                                            uidState, uidState, flags);
-
-                                    if (accessTime <= 0 && rejectTime <= 0 && accessDuration <= 0
-                                            && proxy == null) {
-                                        continue;
-                                    }
-
-                                    String proxyPkg = null;
-                                    String proxyAttributionTag = null;
-                                    int proxyUid = Process.INVALID_UID;
-                                    if (proxy != null) {
-                                        proxyPkg = proxy.getPackageName();
-                                        proxyAttributionTag = proxy.getAttributionTag();
-                                        proxyUid = proxy.getUid();
-                                    }
-
-                                    out.startTag(null, "st");
-                                    if (attributionTag != null) {
-                                        out.attribute(null, "id", attributionTag);
-                                    }
-                                    out.attributeLong(null, "n", key);
-                                    if (accessTime > 0) {
-                                        out.attributeLong(null, "t", accessTime);
-                                    }
-                                    if (rejectTime > 0) {
-                                        out.attributeLong(null, "r", rejectTime);
-                                    }
-                                    if (accessDuration > 0) {
-                                        out.attributeLong(null, "d", accessDuration);
-                                    }
-                                    if (proxyPkg != null) {
-                                        out.attribute(null, "pp", proxyPkg);
-                                    }
-                                    if (proxyAttributionTag != null) {
-                                        out.attribute(null, "pc", proxyAttributionTag);
-                                    }
-                                    if (proxyUid >= 0) {
-                                        out.attributeInt(null, "pu", proxyUid);
-                                    }
-                                    out.endTag(null, "st");
-                                }
-                            }
-
-                            out.endTag(null, "op");
-                        }
-                        out.endTag(null, "uid");
-                    }
-                    if (lastPkg != null) {
-                        out.endTag(null, "pkg");
-                    }
-                }
-
-                out.endTag(null, "app-ops");
-                out.endDocument();
-                mFile.finishWrite(stream);
-            } catch (IOException e) {
-                Slog.w(TAG, "Failed to write state, restoring backup.", e);
-                mFile.failWrite(stream);
-            }
-        }
-        mHistoricalRegistry.writeAndClearDiscreteHistory();
-    }
-
-    private void dumpHelp(PrintWriter pw) {
-        pw.println("AppOps service (appops) dump options:");
-        pw.println("  -h");
-        pw.println("    Print this help text.");
-        pw.println("  --op [OP]");
-        pw.println("    Limit output to data associated with the given app op code.");
-        pw.println("  --mode [MODE]");
-        pw.println("    Limit output to data associated with the given app op mode.");
-        pw.println("  --package [PACKAGE]");
-        pw.println("    Limit output to data associated with the given package name.");
-        pw.println("  --attributionTag [attributionTag]");
-        pw.println("    Limit output to data associated with the given attribution tag.");
-        pw.println("  --include-discrete [n]");
-        pw.println("    Include discrete ops limited to n per dimension. Use zero for no limit.");
-        pw.println("  --watchers");
-        pw.println("    Only output the watcher sections.");
-        pw.println("  --history");
-        pw.println("    Only output history.");
-        pw.println("  --uid-state-changes");
-        pw.println("    Include logs about uid state changes.");
-    }
-
-    private void dumpStatesLocked(@NonNull PrintWriter pw, @Nullable String filterAttributionTag,
-            @HistoricalOpsRequestFilter int filter, long nowElapsed, @NonNull Op op, long now,
-            @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix) {
-        final int numAttributions = op.mAttributions.size();
-        for (int i = 0; i < numAttributions; i++) {
-            if ((filter & FILTER_BY_ATTRIBUTION_TAG) != 0 && !Objects.equals(
-                    op.mAttributions.keyAt(i), filterAttributionTag)) {
-                continue;
-            }
-
-            pw.print(prefix + op.mAttributions.keyAt(i) + "=[\n");
-            dumpStatesLocked(pw, nowElapsed, op, op.mAttributions.keyAt(i), now, sdf, date,
-                    prefix + "  ");
-            pw.print(prefix + "]\n");
-        }
-    }
-
-    private void dumpStatesLocked(@NonNull PrintWriter pw, long nowElapsed, @NonNull Op op,
-            @Nullable String attributionTag, long now, @NonNull SimpleDateFormat sdf,
-            @NonNull Date date, @NonNull String prefix) {
-
-        final AttributedOpEntry entry = op.createSingleAttributionEntryLocked(
-                attributionTag).getAttributedOpEntries().get(attributionTag);
-
-        final ArraySet<Long> keys = entry.collectKeys();
-
-        final int keyCount = keys.size();
-        for (int k = 0; k < keyCount; k++) {
-            final long key = keys.valueAt(k);
-
-            final int uidState = AppOpsManager.extractUidStateFromKey(key);
-            final int flags = AppOpsManager.extractFlagsFromKey(key);
-
-            final long accessTime = entry.getLastAccessTime(uidState, uidState, flags);
-            final long rejectTime = entry.getLastRejectTime(uidState, uidState, flags);
-            final long accessDuration = entry.getLastDuration(uidState, uidState, flags);
-            final OpEventProxyInfo proxy = entry.getLastProxyInfo(uidState, uidState, flags);
-
-            String proxyPkg = null;
-            String proxyAttributionTag = null;
-            int proxyUid = Process.INVALID_UID;
-            if (proxy != null) {
-                proxyPkg = proxy.getPackageName();
-                proxyAttributionTag = proxy.getAttributionTag();
-                proxyUid = proxy.getUid();
-            }
-
-            if (accessTime > 0) {
-                pw.print(prefix);
-                pw.print("Access: ");
-                pw.print(AppOpsManager.keyToString(key));
-                pw.print(" ");
-                date.setTime(accessTime);
-                pw.print(sdf.format(date));
-                pw.print(" (");
-                TimeUtils.formatDuration(accessTime - now, pw);
-                pw.print(")");
-                if (accessDuration > 0) {
-                    pw.print(" duration=");
-                    TimeUtils.formatDuration(accessDuration, pw);
-                }
-                if (proxyUid >= 0) {
-                    pw.print(" proxy[");
-                    pw.print("uid=");
-                    pw.print(proxyUid);
-                    pw.print(", pkg=");
-                    pw.print(proxyPkg);
-                    pw.print(", attributionTag=");
-                    pw.print(proxyAttributionTag);
-                    pw.print("]");
-                }
-                pw.println();
-            }
-
-            if (rejectTime > 0) {
-                pw.print(prefix);
-                pw.print("Reject: ");
-                pw.print(AppOpsManager.keyToString(key));
-                date.setTime(rejectTime);
-                pw.print(sdf.format(date));
-                pw.print(" (");
-                TimeUtils.formatDuration(rejectTime - now, pw);
-                pw.print(")");
-                if (proxyUid >= 0) {
-                    pw.print(" proxy[");
-                    pw.print("uid=");
-                    pw.print(proxyUid);
-                    pw.print(", pkg=");
-                    pw.print(proxyPkg);
-                    pw.print(", attributionTag=");
-                    pw.print(proxyAttributionTag);
-                    pw.print("]");
-                }
-                pw.println();
-            }
-        }
-
-        final AttributedOp attributedOp = op.mAttributions.get(attributionTag);
-        if (attributedOp.isRunning()) {
-            long earliestElapsedTime = Long.MAX_VALUE;
-            long maxNumStarts = 0;
-            int numInProgressEvents = attributedOp.mInProgressEvents.size();
-            for (int i = 0; i < numInProgressEvents; i++) {
-                AttributedOp.InProgressStartOpEvent event =
-                        attributedOp.mInProgressEvents.valueAt(i);
-
-                earliestElapsedTime = Math.min(earliestElapsedTime, event.getStartElapsedTime());
-                maxNumStarts = Math.max(maxNumStarts, event.mNumUnfinishedStarts);
-            }
-
-            pw.print(prefix + "Running start at: ");
-            TimeUtils.formatDuration(nowElapsed - earliestElapsedTime, pw);
-            pw.println();
-
-            if (maxNumStarts > 1) {
-                pw.print(prefix + "startNesting=");
-                pw.println(maxNumStarts);
-            }
-        }
-    }
-
-    @NeverCompile // Avoid size overhead of debugging code.
-    @Override
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return;
-
-        int dumpOp = OP_NONE;
-        String dumpPackage = null;
-        String dumpAttributionTag = null;
-        int dumpUid = Process.INVALID_UID;
-        int dumpMode = -1;
-        boolean dumpWatchers = false;
-        // TODO ntmyren: Remove the dumpHistory and dumpFilter
-        boolean dumpHistory = false;
-        boolean includeDiscreteOps = false;
-        boolean dumpUidStateChangeLogs = false;
-        int nDiscreteOps = 10;
-        @HistoricalOpsRequestFilter int dumpFilter = 0;
-        boolean dumpAll = false;
-
-        if (args != null) {
-            for (int i = 0; i < args.length; i++) {
-                String arg = args[i];
-                if ("-h".equals(arg)) {
-                    dumpHelp(pw);
-                    return;
-                } else if ("-a".equals(arg)) {
-                    // dump all data
-                    dumpAll = true;
-                } else if ("--op".equals(arg)) {
-                    i++;
-                    if (i >= args.length) {
-                        pw.println("No argument for --op option");
-                        return;
-                    }
-                    dumpOp = AppOpsService.Shell.strOpToOp(args[i], pw);
-                    dumpFilter |= FILTER_BY_OP_NAMES;
-                    if (dumpOp < 0) {
-                        return;
-                    }
-                } else if ("--package".equals(arg)) {
-                    i++;
-                    if (i >= args.length) {
-                        pw.println("No argument for --package option");
-                        return;
-                    }
-                    dumpPackage = args[i];
-                    dumpFilter |= FILTER_BY_PACKAGE_NAME;
-                    try {
-                        dumpUid = AppGlobals.getPackageManager().getPackageUid(dumpPackage,
-                                PackageManager.MATCH_KNOWN_PACKAGES | PackageManager.MATCH_INSTANT,
-                                0);
-                    } catch (RemoteException e) {
-                    }
-                    if (dumpUid < 0) {
-                        pw.println("Unknown package: " + dumpPackage);
-                        return;
-                    }
-                    dumpUid = UserHandle.getAppId(dumpUid);
-                    dumpFilter |= FILTER_BY_UID;
-                } else if ("--attributionTag".equals(arg)) {
-                    i++;
-                    if (i >= args.length) {
-                        pw.println("No argument for --attributionTag option");
-                        return;
-                    }
-                    dumpAttributionTag = args[i];
-                    dumpFilter |= FILTER_BY_ATTRIBUTION_TAG;
-                } else if ("--mode".equals(arg)) {
-                    i++;
-                    if (i >= args.length) {
-                        pw.println("No argument for --mode option");
-                        return;
-                    }
-                    dumpMode = AppOpsService.Shell.strModeToMode(args[i], pw);
-                    if (dumpMode < 0) {
-                        return;
-                    }
-                } else if ("--watchers".equals(arg)) {
-                    dumpWatchers = true;
-                } else if ("--include-discrete".equals(arg)) {
-                    i++;
-                    if (i >= args.length) {
-                        pw.println("No argument for --include-discrete option");
-                        return;
-                    }
-                    try {
-                        nDiscreteOps = Integer.valueOf(args[i]);
-                    } catch (NumberFormatException e) {
-                        pw.println("Wrong parameter: " + args[i]);
-                        return;
-                    }
-                    includeDiscreteOps = true;
-                } else if ("--history".equals(arg)) {
-                    dumpHistory = true;
-                } else if (arg.length() > 0 && arg.charAt(0) == '-') {
-                    pw.println("Unknown option: " + arg);
-                    return;
-                } else if ("--uid-state-changes".equals(arg)) {
-                    dumpUidStateChangeLogs = true;
-                } else {
-                    pw.println("Unknown command: " + arg);
-                    return;
-                }
-            }
-        }
-
-        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-        final Date date = new Date();
-        synchronized (this) {
-            pw.println("Current AppOps Service state:");
-            if (!dumpHistory && !dumpWatchers) {
-                mConstants.dump(pw);
-            }
-            pw.println();
-            final long now = System.currentTimeMillis();
-            final long nowElapsed = SystemClock.elapsedRealtime();
-            boolean needSep = false;
-            if (dumpFilter == 0 && dumpMode < 0 && mProfileOwners != null && !dumpWatchers
-                    && !dumpHistory) {
-                pw.println("  Profile owners:");
-                for (int poi = 0; poi < mProfileOwners.size(); poi++) {
-                    pw.print("    User #");
-                    pw.print(mProfileOwners.keyAt(poi));
-                    pw.print(": ");
-                    UserHandle.formatUid(pw, mProfileOwners.valueAt(poi));
-                    pw.println();
-                }
-                pw.println();
-            }
-
-            if (!dumpHistory) {
-                needSep |= mAppOpsServiceInterface.dumpListeners(dumpOp, dumpUid, dumpPackage, pw);
-            }
-
-            if (mModeWatchers.size() > 0 && dumpOp < 0 && !dumpHistory) {
-                boolean printedHeader = false;
-                for (int i = 0; i < mModeWatchers.size(); i++) {
-                    final ModeCallback cb = mModeWatchers.valueAt(i);
-                    if (dumpPackage != null
-                            && dumpUid != UserHandle.getAppId(cb.getWatchingUid())) {
-                        continue;
-                    }
-                    needSep = true;
-                    if (!printedHeader) {
-                        pw.println("  All op mode watchers:");
-                        printedHeader = true;
-                    }
-                    pw.print("    ");
-                    pw.print(Integer.toHexString(System.identityHashCode(mModeWatchers.keyAt(i))));
-                    pw.print(": ");
-                    pw.println(cb);
-                }
-            }
-            if (mActiveWatchers.size() > 0 && dumpMode < 0) {
-                needSep = true;
-                boolean printedHeader = false;
-                for (int watcherNum = 0; watcherNum < mActiveWatchers.size(); watcherNum++) {
-                    final SparseArray<ActiveCallback> activeWatchers =
-                            mActiveWatchers.valueAt(watcherNum);
-                    if (activeWatchers.size() <= 0) {
-                        continue;
-                    }
-                    final ActiveCallback cb = activeWatchers.valueAt(0);
-                    if (dumpOp >= 0 && activeWatchers.indexOfKey(dumpOp) < 0) {
-                        continue;
-                    }
-                    if (dumpPackage != null
-                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
-                        continue;
-                    }
-                    if (!printedHeader) {
-                        pw.println("  All op active watchers:");
-                        printedHeader = true;
-                    }
-                    pw.print("    ");
-                    pw.print(Integer.toHexString(System.identityHashCode(
-                            mActiveWatchers.keyAt(watcherNum))));
-                    pw.println(" ->");
-                    pw.print("        [");
-                    final int opCount = activeWatchers.size();
-                    for (int opNum = 0; opNum < opCount; opNum++) {
-                        if (opNum > 0) {
-                            pw.print(' ');
-                        }
-                        pw.print(AppOpsManager.opToName(activeWatchers.keyAt(opNum)));
-                        if (opNum < opCount - 1) {
-                            pw.print(',');
-                        }
-                    }
-                    pw.println("]");
-                    pw.print("        ");
-                    pw.println(cb);
-                }
-            }
-            if (mStartedWatchers.size() > 0 && dumpMode < 0) {
-                needSep = true;
-                boolean printedHeader = false;
-
-                final int watchersSize = mStartedWatchers.size();
-                for (int watcherNum = 0; watcherNum < watchersSize; watcherNum++) {
-                    final SparseArray<StartedCallback> startedWatchers =
-                            mStartedWatchers.valueAt(watcherNum);
-                    if (startedWatchers.size() <= 0) {
-                        continue;
-                    }
-
-                    final StartedCallback cb = startedWatchers.valueAt(0);
-                    if (dumpOp >= 0 && startedWatchers.indexOfKey(dumpOp) < 0) {
-                        continue;
-                    }
-
-                    if (dumpPackage != null
-                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
-                        continue;
-                    }
-
-                    if (!printedHeader) {
-                        pw.println("  All op started watchers:");
-                        printedHeader = true;
-                    }
-
-                    pw.print("    ");
-                    pw.print(Integer.toHexString(System.identityHashCode(
-                            mStartedWatchers.keyAt(watcherNum))));
-                    pw.println(" ->");
-
-                    pw.print("        [");
-                    final int opCount = startedWatchers.size();
-                    for (int opNum = 0; opNum < opCount; opNum++) {
-                        if (opNum > 0) {
-                            pw.print(' ');
-                        }
-
-                        pw.print(AppOpsManager.opToName(startedWatchers.keyAt(opNum)));
-                        if (opNum < opCount - 1) {
-                            pw.print(',');
-                        }
-                    }
-                    pw.println("]");
-
-                    pw.print("        ");
-                    pw.println(cb);
-                }
-            }
-            if (mNotedWatchers.size() > 0 && dumpMode < 0) {
-                needSep = true;
-                boolean printedHeader = false;
-                for (int watcherNum = 0; watcherNum < mNotedWatchers.size(); watcherNum++) {
-                    final SparseArray<NotedCallback> notedWatchers =
-                            mNotedWatchers.valueAt(watcherNum);
-                    if (notedWatchers.size() <= 0) {
-                        continue;
-                    }
-                    final NotedCallback cb = notedWatchers.valueAt(0);
-                    if (dumpOp >= 0 && notedWatchers.indexOfKey(dumpOp) < 0) {
-                        continue;
-                    }
-                    if (dumpPackage != null
-                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
-                        continue;
-                    }
-                    if (!printedHeader) {
-                        pw.println("  All op noted watchers:");
-                        printedHeader = true;
-                    }
-                    pw.print("    ");
-                    pw.print(Integer.toHexString(System.identityHashCode(
-                            mNotedWatchers.keyAt(watcherNum))));
-                    pw.println(" ->");
-                    pw.print("        [");
-                    final int opCount = notedWatchers.size();
-                    for (int opNum = 0; opNum < opCount; opNum++) {
-                        if (opNum > 0) {
-                            pw.print(' ');
-                        }
-                        pw.print(AppOpsManager.opToName(notedWatchers.keyAt(opNum)));
-                        if (opNum < opCount - 1) {
-                            pw.print(',');
-                        }
-                    }
-                    pw.println("]");
-                    pw.print("        ");
-                    pw.println(cb);
-                }
-            }
-            if (needSep) {
-                pw.println();
-            }
-            for (int i = 0; i < mUidStates.size(); i++) {
-                UidState uidState = mUidStates.valueAt(i);
-                final SparseIntArray opModes = uidState.getNonDefaultUidModes();
-                final ArrayMap<String, Ops> pkgOps = uidState.pkgOps;
-
-                if (dumpWatchers || dumpHistory) {
-                    continue;
-                }
-                if (dumpOp >= 0 || dumpPackage != null || dumpMode >= 0) {
-                    boolean hasOp = dumpOp < 0 || (opModes != null
-                            && opModes.indexOfKey(dumpOp) >= 0);
-                    boolean hasPackage = dumpPackage == null || dumpUid == mUidStates.keyAt(i);
-                    boolean hasMode = dumpMode < 0;
-                    if (!hasMode && opModes != null) {
-                        for (int opi = 0; !hasMode && opi < opModes.size(); opi++) {
-                            if (opModes.valueAt(opi) == dumpMode) {
-                                hasMode = true;
-                            }
-                        }
-                    }
-                    if (pkgOps != null) {
-                        for (int pkgi = 0;
-                                (!hasOp || !hasPackage || !hasMode) && pkgi < pkgOps.size();
-                                pkgi++) {
-                            Ops ops = pkgOps.valueAt(pkgi);
-                            if (!hasOp && ops != null && ops.indexOfKey(dumpOp) >= 0) {
-                                hasOp = true;
-                            }
-                            if (!hasMode) {
-                                for (int opi = 0; !hasMode && opi < ops.size(); opi++) {
-                                    if (ops.valueAt(opi).getMode() == dumpMode) {
-                                        hasMode = true;
-                                    }
-                                }
-                            }
-                            if (!hasPackage && dumpPackage.equals(ops.packageName)) {
-                                hasPackage = true;
-                            }
-                        }
-                    }
-                    if (uidState.foregroundOps != null && !hasOp) {
-                        if (uidState.foregroundOps.indexOfKey(dumpOp) > 0) {
-                            hasOp = true;
-                        }
-                    }
-                    if (!hasOp || !hasPackage || !hasMode) {
-                        continue;
-                    }
-                }
-
-                pw.print("  Uid ");
-                UserHandle.formatUid(pw, uidState.uid);
-                pw.println(":");
-                uidState.dump(pw, nowElapsed);
-                if (uidState.foregroundOps != null && (dumpMode < 0
-                        || dumpMode == AppOpsManager.MODE_FOREGROUND)) {
-                    pw.println("    foregroundOps:");
-                    for (int j = 0; j < uidState.foregroundOps.size(); j++) {
-                        if (dumpOp >= 0 && dumpOp != uidState.foregroundOps.keyAt(j)) {
-                            continue;
-                        }
-                        pw.print("      ");
-                        pw.print(AppOpsManager.opToName(uidState.foregroundOps.keyAt(j)));
-                        pw.print(": ");
-                        pw.println(uidState.foregroundOps.valueAt(j) ? "WATCHER" : "SILENT");
-                    }
-                    pw.print("    hasForegroundWatchers=");
-                    pw.println(uidState.hasForegroundWatchers);
-                }
-                needSep = true;
-
-                if (opModes != null) {
-                    final int opModeCount = opModes.size();
-                    for (int j = 0; j < opModeCount; j++) {
-                        final int code = opModes.keyAt(j);
-                        final int mode = opModes.valueAt(j);
-                        if (dumpOp >= 0 && dumpOp != code) {
-                            continue;
-                        }
-                        if (dumpMode >= 0 && dumpMode != mode) {
-                            continue;
-                        }
-                        pw.print("      ");
-                        pw.print(AppOpsManager.opToName(code));
-                        pw.print(": mode=");
-                        pw.println(AppOpsManager.modeToName(mode));
-                    }
-                }
-
-                if (pkgOps == null) {
-                    continue;
-                }
-
-                for (int pkgi = 0; pkgi < pkgOps.size(); pkgi++) {
-                    final Ops ops = pkgOps.valueAt(pkgi);
-                    if (dumpPackage != null && !dumpPackage.equals(ops.packageName)) {
-                        continue;
-                    }
-                    boolean printedPackage = false;
-                    for (int j = 0; j < ops.size(); j++) {
-                        final Op op = ops.valueAt(j);
-                        final int opCode = op.op;
-                        if (dumpOp >= 0 && dumpOp != opCode) {
-                            continue;
-                        }
-                        if (dumpMode >= 0 && dumpMode != op.getMode()) {
-                            continue;
-                        }
-                        if (!printedPackage) {
-                            pw.print("    Package ");
-                            pw.print(ops.packageName);
-                            pw.println(":");
-                            printedPackage = true;
-                        }
-                        pw.print("      ");
-                        pw.print(AppOpsManager.opToName(opCode));
-                        pw.print(" (");
-                        pw.print(AppOpsManager.modeToName(op.getMode()));
-                        final int switchOp = AppOpsManager.opToSwitch(opCode);
-                        if (switchOp != opCode) {
-                            pw.print(" / switch ");
-                            pw.print(AppOpsManager.opToName(switchOp));
-                            final Op switchObj = ops.get(switchOp);
-                            int mode = switchObj == null
-                                    ? AppOpsManager.opToDefaultMode(switchOp) : switchObj.getMode();
-                            pw.print("=");
-                            pw.print(AppOpsManager.modeToName(mode));
-                        }
-                        pw.println("): ");
-                        dumpStatesLocked(pw, dumpAttributionTag, dumpFilter, nowElapsed, op, now,
-                                sdf, date, "        ");
-                    }
-                }
-            }
-            if (needSep) {
-                pw.println();
-            }
-
-            boolean showUserRestrictions = !(dumpMode < 0 && !dumpWatchers && !dumpHistory);
-            mAppOpsRestrictions.dumpRestrictions(pw, dumpOp, dumpPackage, showUserRestrictions);
-
-            if (dumpAll || dumpUidStateChangeLogs) {
-                pw.println();
-                pw.println("Uid State Changes Event Log:");
-                getUidStateTracker().dumpEvents(pw);
-            }
-        }
-
-        // Must not hold the appops lock
-        if (dumpHistory && !dumpWatchers) {
-            mHistoricalRegistry.dump("  ", pw, dumpUid, dumpPackage, dumpAttributionTag, dumpOp,
-                    dumpFilter);
-        }
-        if (includeDiscreteOps) {
-            pw.println("Discrete accesses: ");
-            mHistoricalRegistry.dumpDiscreteData(pw, dumpUid, dumpPackage, dumpAttributionTag,
-                    dumpFilter, dumpOp, sdf, date, "  ", nDiscreteOps);
-        }
-    }
-
-    @Override
-    public void setUserRestrictions(Bundle restrictions, IBinder token, int userHandle) {
-        checkSystemUid("setUserRestrictions");
-        Objects.requireNonNull(restrictions);
-        Objects.requireNonNull(token);
-        for (int i = 0; i < AppOpsManager._NUM_OP; i++) {
-            String restriction = AppOpsManager.opToRestriction(i);
-            if (restriction != null) {
-                setUserRestrictionNoCheck(i, restrictions.getBoolean(restriction, false), token,
-                        userHandle, null);
-            }
-        }
-    }
-
-    @Override
-    public void setUserRestriction(int code, boolean restricted, IBinder token, int userHandle,
-            PackageTagsList excludedPackageTags) {
-        if (Binder.getCallingPid() != Process.myPid()) {
-            mContext.enforcePermission(Manifest.permission.MANAGE_APP_OPS_RESTRICTIONS,
-                    Binder.getCallingPid(), Binder.getCallingUid(), null);
-        }
-        if (userHandle != UserHandle.getCallingUserId()) {
-            if (mContext.checkCallingOrSelfPermission(Manifest.permission
-                    .INTERACT_ACROSS_USERS_FULL) != PackageManager.PERMISSION_GRANTED
-                    && mContext.checkCallingOrSelfPermission(Manifest.permission
-                    .INTERACT_ACROSS_USERS) != PackageManager.PERMISSION_GRANTED) {
-                throw new SecurityException("Need INTERACT_ACROSS_USERS_FULL or"
-                        + " INTERACT_ACROSS_USERS to interact cross user ");
-            }
-        }
-        verifyIncomingOp(code);
-        Objects.requireNonNull(token);
-        setUserRestrictionNoCheck(code, restricted, token, userHandle, excludedPackageTags);
-    }
-
-    private void setUserRestrictionNoCheck(int code, boolean restricted, IBinder token,
-            int userHandle, PackageTagsList excludedPackageTags) {
-        synchronized (AppOpsServiceImpl.this) {
-            ClientUserRestrictionState restrictionState = mOpUserRestrictions.get(token);
-
-            if (restrictionState == null) {
-                try {
-                    restrictionState = new ClientUserRestrictionState(token);
-                } catch (RemoteException e) {
-                    return;
-                }
-                mOpUserRestrictions.put(token, restrictionState);
-            }
-
-            if (restrictionState.setRestriction(code, restricted, excludedPackageTags,
-                    userHandle)) {
-                mHandler.sendMessage(PooledLambda.obtainMessage(
-                        AppOpsServiceImpl::notifyWatchersOfChange, this, code, UID_ANY));
-                mHandler.sendMessage(PooledLambda.obtainMessage(
-                        AppOpsServiceImpl::updateStartedOpModeForUser, this, code,
-                        restricted, userHandle));
-            }
-
-            if (restrictionState.isDefault()) {
-                mOpUserRestrictions.remove(token);
-                restrictionState.destroy();
-            }
-        }
-    }
-
-    @Override
-    public void setGlobalRestriction(int code, boolean restricted, IBinder token) {
-        if (Binder.getCallingPid() != Process.myPid()) {
-            throw new SecurityException("Only the system can set global restrictions");
-        }
-
-        synchronized (this) {
-            ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.get(token);
-
-            if (restrictionState == null) {
-                try {
-                    restrictionState = new ClientGlobalRestrictionState(token);
-                } catch (RemoteException e) {
-                    return;
-                }
-                mOpGlobalRestrictions.put(token, restrictionState);
-            }
-
-            if (restrictionState.setRestriction(code, restricted)) {
-                mHandler.sendMessage(PooledLambda.obtainMessage(
-                        AppOpsServiceImpl::notifyWatchersOfChange, this, code, UID_ANY));
-                mHandler.sendMessage(PooledLambda.obtainMessage(
-                        AppOpsServiceImpl::updateStartedOpModeForUser, this, code,
-                        restricted, UserHandle.USER_ALL));
-            }
-
-            if (restrictionState.isDefault()) {
-                mOpGlobalRestrictions.remove(token);
-                restrictionState.destroy();
-            }
-        }
-    }
-
-    @Override
-    public int getOpRestrictionCount(int code, UserHandle user, String pkg,
-            String attributionTag) {
-        int number = 0;
-        synchronized (this) {
-            int numRestrictions = mOpUserRestrictions.size();
-            for (int i = 0; i < numRestrictions; i++) {
-                if (mOpUserRestrictions.valueAt(i)
-                        .hasRestriction(code, pkg, attributionTag, user.getIdentifier(),
-                                false)) {
-                    number++;
-                }
-            }
-
-            numRestrictions = mOpGlobalRestrictions.size();
-            for (int i = 0; i < numRestrictions; i++) {
-                if (mOpGlobalRestrictions.valueAt(i).hasRestriction(code)) {
-                    number++;
-                }
-            }
-        }
-
-        return number;
-    }
-
-    private void updateStartedOpModeForUser(int code, boolean restricted, int userId) {
-        synchronized (AppOpsServiceImpl.this) {
-            int numUids = mUidStates.size();
-            for (int uidNum = 0; uidNum < numUids; uidNum++) {
-                int uid = mUidStates.keyAt(uidNum);
-                if (userId != UserHandle.USER_ALL && UserHandle.getUserId(uid) != userId) {
-                    continue;
-                }
-                updateStartedOpModeForUidLocked(code, restricted, uid);
-            }
-        }
-    }
-
-    private void updateStartedOpModeForUidLocked(int code, boolean restricted, int uid) {
-        UidState uidState = mUidStates.get(uid);
-        if (uidState == null || uidState.pkgOps == null) {
-            return;
-        }
-
-        int numPkgOps = uidState.pkgOps.size();
-        for (int pkgNum = 0; pkgNum < numPkgOps; pkgNum++) {
-            Ops ops = uidState.pkgOps.valueAt(pkgNum);
-            Op op = ops != null ? ops.get(code) : null;
-            if (op == null || (op.getMode() != MODE_ALLOWED && op.getMode() != MODE_FOREGROUND)) {
-                continue;
-            }
-            int numAttrTags = op.mAttributions.size();
-            for (int attrNum = 0; attrNum < numAttrTags; attrNum++) {
-                AttributedOp attrOp = op.mAttributions.valueAt(attrNum);
-                if (restricted && attrOp.isRunning()) {
-                    attrOp.pause();
-                } else if (attrOp.isPaused()) {
-                    attrOp.resume();
-                }
-            }
-        }
-    }
-
-    @Override
-    public void notifyWatchersOfChange(int code, int uid) {
-        final ArraySet<OnOpModeChangedListener> modeChangedListenerSet;
-        synchronized (this) {
-            modeChangedListenerSet = mAppOpsServiceInterface.getOpModeChangedListeners(code);
-            if (modeChangedListenerSet == null) {
-                return;
-            }
-        }
-
-        notifyOpChanged(modeChangedListenerSet, code, uid, null);
-    }
-
-    @Override
-    public void removeUser(int userHandle) throws RemoteException {
-        checkSystemUid("removeUser");
-        synchronized (AppOpsServiceImpl.this) {
-            final int tokenCount = mOpUserRestrictions.size();
-            for (int i = tokenCount - 1; i >= 0; i--) {
-                ClientUserRestrictionState opRestrictions = mOpUserRestrictions.valueAt(i);
-                opRestrictions.removeUser(userHandle);
-            }
-            removeUidsForUserLocked(userHandle);
-        }
-    }
-
-    @Override
-    public boolean isOperationActive(int code, int uid, String packageName) {
-        if (Binder.getCallingUid() != uid) {
-            if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
-                    != PackageManager.PERMISSION_GRANTED) {
-                return false;
-            }
-        }
-        verifyIncomingOp(code);
-        if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) {
-            return false;
-        }
-
-        final String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName);
-        if (resolvedPackageName == null) {
-            return false;
-        }
-        // TODO moltmann: Allow to check for attribution op activeness
-        synchronized (AppOpsServiceImpl.this) {
-            Ops pkgOps = getOpsLocked(uid, resolvedPackageName, null, false, null, false);
-            if (pkgOps == null) {
-                return false;
-            }
-
-            Op op = pkgOps.get(code);
-            if (op == null) {
-                return false;
-            }
-
-            return op.isRunning();
-        }
-    }
-
-    @Override
-    public boolean isProxying(int op, @NonNull String proxyPackageName,
-            @NonNull String proxyAttributionTag, int proxiedUid,
-            @NonNull String proxiedPackageName) {
-        Objects.requireNonNull(proxyPackageName);
-        Objects.requireNonNull(proxiedPackageName);
-        final long callingUid = Binder.getCallingUid();
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            final List<AppOpsManager.PackageOps> packageOps = getOpsForPackage(proxiedUid,
-                    proxiedPackageName, new int[]{op});
-            if (packageOps == null || packageOps.isEmpty()) {
-                return false;
-            }
-            final List<OpEntry> opEntries = packageOps.get(0).getOps();
-            if (opEntries.isEmpty()) {
-                return false;
-            }
-            final OpEntry opEntry = opEntries.get(0);
-            if (!opEntry.isRunning()) {
-                return false;
-            }
-            final OpEventProxyInfo proxyInfo = opEntry.getLastProxyInfo(
-                    OP_FLAG_TRUSTED_PROXIED | AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED);
-            return proxyInfo != null && callingUid == proxyInfo.getUid()
-                    && proxyPackageName.equals(proxyInfo.getPackageName())
-                    && Objects.equals(proxyAttributionTag, proxyInfo.getAttributionTag());
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    @Override
-    public void resetPackageOpsNoHistory(@NonNull String packageName) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
-                "resetPackageOpsNoHistory");
-        synchronized (AppOpsServiceImpl.this) {
-            final int uid = mPackageManagerInternal.getPackageUid(packageName, 0,
-                    UserHandle.getCallingUserId());
-            if (uid == Process.INVALID_UID) {
-                return;
-            }
-            UidState uidState = mUidStates.get(uid);
-            if (uidState == null || uidState.pkgOps == null) {
-                return;
-            }
-            Ops removedOps = uidState.pkgOps.remove(packageName);
-            mAppOpsServiceInterface.removePackage(packageName, UserHandle.getUserId(uid));
-            if (removedOps != null) {
-                scheduleFastWriteLocked();
-            }
-        }
-    }
-
-    @Override
-    public void setHistoryParameters(@AppOpsManager.HistoricalMode int mode,
-            long baseSnapshotInterval, int compressionStep) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
-                "setHistoryParameters");
-        // Must not hold the appops lock
-        mHistoricalRegistry.setHistoryParameters(mode, baseSnapshotInterval, compressionStep);
-    }
-
-    @Override
-    public void offsetHistory(long offsetMillis) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
-                "offsetHistory");
-        // Must not hold the appops lock
-        mHistoricalRegistry.offsetHistory(offsetMillis);
-        mHistoricalRegistry.offsetDiscreteHistory(offsetMillis);
-    }
-
-    @Override
-    public void addHistoricalOps(HistoricalOps ops) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
-                "addHistoricalOps");
-        // Must not hold the appops lock
-        mHistoricalRegistry.addHistoricalOps(ops);
-    }
-
-    @Override
-    public void resetHistoryParameters() {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
-                "resetHistoryParameters");
-        // Must not hold the appops lock
-        mHistoricalRegistry.resetHistoryParameters();
-    }
-
-    @Override
-    public void clearHistory() {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
-                "clearHistory");
-        // Must not hold the appops lock
-        mHistoricalRegistry.clearAllHistory();
-    }
-
-    @Override
-    public void rebootHistory(long offlineDurationMillis) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
-                "rebootHistory");
-
-        Preconditions.checkArgument(offlineDurationMillis >= 0);
-
-        // Must not hold the appops lock
-        mHistoricalRegistry.shutdown();
-
-        if (offlineDurationMillis > 0) {
-            SystemClock.sleep(offlineDurationMillis);
-        }
-
-        mHistoricalRegistry = new HistoricalRegistry(mHistoricalRegistry);
-        mHistoricalRegistry.systemReady(mContext.getContentResolver());
-        mHistoricalRegistry.persistPendingHistory();
-    }
-
-    @GuardedBy("this")
-    private void removeUidsForUserLocked(int userHandle) {
-        for (int i = mUidStates.size() - 1; i >= 0; --i) {
-            final int uid = mUidStates.keyAt(i);
-            if (UserHandle.getUserId(uid) == userHandle) {
-                mUidStates.valueAt(i).clear();
-                mUidStates.removeAt(i);
-            }
-        }
-    }
-
-    private void checkSystemUid(String function) {
-        int uid = Binder.getCallingUid();
-        if (uid != Process.SYSTEM_UID) {
-            throw new SecurityException(function + " must by called by the system");
-        }
-    }
-
-    private static int resolveUid(String packageName) {
-        if (packageName == null) {
-            return Process.INVALID_UID;
-        }
-        switch (packageName) {
-            case "root":
-                return Process.ROOT_UID;
-            case "shell":
-            case "dumpstate":
-                return Process.SHELL_UID;
-            case "media":
-                return Process.MEDIA_UID;
-            case "audioserver":
-                return Process.AUDIOSERVER_UID;
-            case "cameraserver":
-                return Process.CAMERASERVER_UID;
-        }
-        return Process.INVALID_UID;
-    }
-
-    private static String[] getPackagesForUid(int uid) {
-        String[] packageNames = null;
-
-        // Very early during boot the package manager is not yet or not yet fully started. At this
-        // time there are no packages yet.
-        if (AppGlobals.getPackageManager() != null) {
-            try {
-                packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid);
-            } catch (RemoteException e) {
-                /* ignore - local call */
-            }
-        }
-        if (packageNames == null) {
-            return EmptyArray.STRING;
-        }
-        return packageNames;
-    }
-
-    private final class ClientUserRestrictionState implements DeathRecipient {
-        private final IBinder mToken;
-
-        ClientUserRestrictionState(IBinder token)
-                throws RemoteException {
-            token.linkToDeath(this, 0);
-            this.mToken = token;
-        }
-
-        public boolean setRestriction(int code, boolean restricted,
-                PackageTagsList excludedPackageTags, int userId) {
-            return mAppOpsRestrictions.setUserRestriction(mToken, userId, code,
-                    restricted, excludedPackageTags);
-        }
-
-        public boolean hasRestriction(int code, String packageName, String attributionTag,
-                int userId, boolean isCheckOp) {
-            return mAppOpsRestrictions.getUserRestriction(mToken, userId, code, packageName,
-                    attributionTag, isCheckOp);
-        }
-
-        public void removeUser(int userId) {
-            mAppOpsRestrictions.clearUserRestrictions(mToken, userId);
-        }
-
-        public boolean isDefault() {
-            return !mAppOpsRestrictions.hasUserRestrictions(mToken);
-        }
-
-        @Override
-        public void binderDied() {
-            synchronized (AppOpsServiceImpl.this) {
-                mAppOpsRestrictions.clearUserRestrictions(mToken);
-                mOpUserRestrictions.remove(mToken);
-                destroy();
-            }
-        }
-
-        public void destroy() {
-            mToken.unlinkToDeath(this, 0);
-        }
-    }
-
-    private final class ClientGlobalRestrictionState implements DeathRecipient {
-        final IBinder mToken;
-
-        ClientGlobalRestrictionState(IBinder token)
-                throws RemoteException {
-            token.linkToDeath(this, 0);
-            this.mToken = token;
-        }
-
-        boolean setRestriction(int code, boolean restricted) {
-            return mAppOpsRestrictions.setGlobalRestriction(mToken, code, restricted);
-        }
-
-        boolean hasRestriction(int code) {
-            return mAppOpsRestrictions.getGlobalRestriction(mToken, code);
-        }
-
-        boolean isDefault() {
-            return !mAppOpsRestrictions.hasGlobalRestrictions(mToken);
-        }
-
-        @Override
-        public void binderDied() {
-            mAppOpsRestrictions.clearGlobalRestrictions(mToken);
-            mOpGlobalRestrictions.remove(mToken);
-            destroy();
-        }
-
-        void destroy() {
-            mToken.unlinkToDeath(this, 0);
-        }
-    }
-
-    @Override
-    public void setDeviceAndProfileOwners(SparseIntArray owners) {
-        synchronized (this) {
-            mProfileOwners = owners;
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsServiceInterface.java
deleted file mode 100644
index 8420fcb..0000000
--- a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java
+++ /dev/null
@@ -1,494 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.appop;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.AppOpsManager;
-import android.content.AttributionSource;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.PackageTagsList;
-import android.os.RemoteCallback;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.SparseArray;
-import android.util.SparseIntArray;
-
-import com.android.internal.app.IAppOpsActiveCallback;
-import com.android.internal.app.IAppOpsCallback;
-import com.android.internal.app.IAppOpsNotedCallback;
-import com.android.internal.app.IAppOpsStartedCallback;
-
-import dalvik.annotation.optimization.NeverCompile;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.List;
-
-/**
- *
- */
-public interface AppOpsServiceInterface extends PersistenceScheduler {
-
-    /**
-     *
-     */
-    void systemReady();
-
-    /**
-     *
-     */
-    void shutdown();
-
-    /**
-     *
-     * @param uid
-     * @param packageName
-     */
-    void verifyPackage(int uid, String packageName);
-
-    /**
-     *
-     * @param op
-     * @param packageName
-     * @param flags
-     * @param callback
-     */
-    void startWatchingModeWithFlags(int op, String packageName, int flags,
-            IAppOpsCallback callback);
-
-    /**
-     *
-     * @param callback
-     */
-    void stopWatchingMode(IAppOpsCallback callback);
-
-    /**
-     *
-     * @param ops
-     * @param callback
-     */
-    void startWatchingActive(int[] ops, IAppOpsActiveCallback callback);
-
-    /**
-     *
-     * @param callback
-     */
-    void stopWatchingActive(IAppOpsActiveCallback callback);
-
-    /**
-     *
-     * @param ops
-     * @param callback
-     */
-    void startWatchingStarted(int[] ops, @NonNull IAppOpsStartedCallback callback);
-
-    /**
-     *
-     * @param callback
-     */
-    void stopWatchingStarted(IAppOpsStartedCallback callback);
-
-    /**
-     *
-     * @param ops
-     * @param callback
-     */
-    void startWatchingNoted(@NonNull int[] ops, @NonNull IAppOpsNotedCallback callback);
-
-    /**
-     *
-     * @param callback
-     */
-    void stopWatchingNoted(IAppOpsNotedCallback callback);
-
-    /**
-     * @param clientId
-     * @param code
-     * @param uid
-     * @param packageName
-     * @param attributionTag
-     * @param startIfModeDefault
-     * @param message
-     * @param attributionFlags
-     * @param attributionChainId
-     * @return
-     */
-    int startOperation(@NonNull IBinder clientId, int code, int uid,
-            @Nullable String packageName, @Nullable String attributionTag,
-            boolean startIfModeDefault, @NonNull String message,
-            @AppOpsManager.AttributionFlags int attributionFlags,
-            int attributionChainId);
-
-
-    int startOperationUnchecked(IBinder clientId, int code, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, int proxyUid, String proxyPackageName,
-            @Nullable String proxyAttributionTag, @AppOpsManager.OpFlags int flags,
-            boolean startIfModeDefault, @AppOpsManager.AttributionFlags int attributionFlags,
-            int attributionChainId, boolean dryRun);
-
-    /**
-     *
-     * @param clientId
-     * @param code
-     * @param uid
-     * @param packageName
-     * @param attributionTag
-     */
-    void finishOperation(IBinder clientId, int code, int uid, String packageName,
-            String attributionTag);
-
-    /**
-     *
-     * @param clientId
-     * @param code
-     * @param uid
-     * @param packageName
-     * @param attributionTag
-     */
-    void finishOperationUnchecked(IBinder clientId, int code, int uid, String packageName,
-            String attributionTag);
-
-    /**
-     *
-     * @param uidPackageNames
-     * @param visible
-     */
-    void updateAppWidgetVisibility(SparseArray<String> uidPackageNames, boolean visible);
-
-    /**
-     *
-     */
-    void readState();
-
-    /**
-     *
-     */
-    void writeState();
-
-    /**
-     *
-     * @param uid
-     * @param packageName
-     */
-    void packageRemoved(int uid, String packageName);
-
-    /**
-     *
-     * @param uid
-     */
-    void uidRemoved(int uid);
-
-    /**
-     *
-     * @param uid
-     * @param procState
-     * @param capability
-     */
-    void updateUidProcState(int uid, int procState,
-            @ActivityManager.ProcessCapability int capability);
-
-    /**
-     *
-     * @param ops
-     * @return
-     */
-    List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops);
-
-    /**
-     *
-     * @param uid
-     * @param packageName
-     * @param ops
-     * @return
-     */
-    List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName,
-            int[] ops);
-
-    /**
-     *
-     * @param uid
-     * @param packageName
-     * @param attributionTag
-     * @param opNames
-     * @param dataType
-     * @param filter
-     * @param beginTimeMillis
-     * @param endTimeMillis
-     * @param flags
-     * @param callback
-     */
-    void getHistoricalOps(int uid, String packageName, String attributionTag,
-            List<String> opNames, int dataType, int filter, long beginTimeMillis,
-            long endTimeMillis, int flags, RemoteCallback callback);
-
-    /**
-     *
-     * @param uid
-     * @param packageName
-     * @param attributionTag
-     * @param opNames
-     * @param dataType
-     * @param filter
-     * @param beginTimeMillis
-     * @param endTimeMillis
-     * @param flags
-     * @param callback
-     */
-    void getHistoricalOpsFromDiskRaw(int uid, String packageName, String attributionTag,
-            List<String> opNames, int dataType, int filter, long beginTimeMillis,
-            long endTimeMillis, int flags, RemoteCallback callback);
-
-    /**
-     *
-     */
-    void reloadNonHistoricalState();
-
-    /**
-     *
-     * @param uid
-     * @param ops
-     * @return
-     */
-    List<AppOpsManager.PackageOps> getUidOps(int uid, int[] ops);
-
-    /**
-     *
-     * @param owners
-     */
-    void setDeviceAndProfileOwners(SparseIntArray owners);
-
-    // used in audio restriction calls, might just copy the logic to avoid having this call.
-    /**
-     *
-     * @param callingPid
-     * @param callingUid
-     * @param targetUid
-     */
-    void enforceManageAppOpsModes(int callingPid, int callingUid, int targetUid);
-
-    /**
-     *
-     * @param code
-     * @param uid
-     * @param mode
-     * @param permissionPolicyCallback
-     */
-    void setUidMode(int code, int uid, int mode,
-            @Nullable IAppOpsCallback permissionPolicyCallback);
-
-    /**
-     *
-     * @param code
-     * @param uid
-     * @param packageName
-     * @param mode
-     * @param permissionPolicyCallback
-     */
-    void setMode(int code, int uid, @NonNull String packageName, int mode,
-            @Nullable IAppOpsCallback permissionPolicyCallback);
-
-    /**
-     *
-     * @param reqUserId
-     * @param reqPackageName
-     */
-    void resetAllModes(int reqUserId, String reqPackageName);
-
-    /**
-     *
-     * @param code
-     * @param uid
-     * @param packageName
-     * @param attributionTag
-     * @param raw
-     * @return
-     */
-    int checkOperation(int code, int uid, String packageName,
-            @Nullable String attributionTag, boolean raw);
-
-    /**
-     *
-     * @param uid
-     * @param packageName
-     * @return
-     */
-    int checkPackage(int uid, String packageName);
-
-    /**
-     *
-     * @param code
-     * @param uid
-     * @param packageName
-     * @param attributionTag
-     * @param message
-     * @return
-     */
-    int noteOperation(int code, int uid, @Nullable String packageName,
-            @Nullable String attributionTag, @Nullable String message);
-
-    /**
-     *
-     * @param code
-     * @param uid
-     * @param packageName
-     * @param attributionTag
-     * @param proxyUid
-     * @param proxyPackageName
-     * @param proxyAttributionTag
-     * @param flags
-     * @return
-     */
-    @AppOpsManager.Mode
-    int noteOperationUnchecked(int code, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, int proxyUid, String proxyPackageName,
-            @Nullable String proxyAttributionTag, @AppOpsManager.OpFlags int flags);
-
-    boolean isAttributionTagValid(int uid, @NonNull String packageName,
-            @Nullable String attributionTag, @Nullable String proxyPackageName);
-
-    /**
-     *
-     * @param fd
-     * @param pw
-     * @param args
-     */
-    @NeverCompile
-        // Avoid size overhead of debugging code.
-    void dump(FileDescriptor fd, PrintWriter pw, String[] args);
-
-    /**
-     *
-     * @param restrictions
-     * @param token
-     * @param userHandle
-     */
-    void setUserRestrictions(Bundle restrictions, IBinder token, int userHandle);
-
-    /**
-     *
-     * @param code
-     * @param restricted
-     * @param token
-     * @param userHandle
-     * @param excludedPackageTags
-     */
-    void setUserRestriction(int code, boolean restricted, IBinder token, int userHandle,
-            PackageTagsList excludedPackageTags);
-
-    /**
-     *
-     * @param code
-     * @param restricted
-     * @param token
-     */
-    void setGlobalRestriction(int code, boolean restricted, IBinder token);
-
-    /**
-     *
-     * @param code
-     * @param user
-     * @param pkg
-     * @param attributionTag
-     * @return
-     */
-    int getOpRestrictionCount(int code, UserHandle user, String pkg,
-            String attributionTag);
-
-    /**
-     *
-     * @param code
-     * @param uid
-     */
-    // added to interface for audio restriction stuff
-    void notifyWatchersOfChange(int code, int uid);
-
-    /**
-     *
-     * @param userHandle
-     * @throws RemoteException
-     */
-    void removeUser(int userHandle) throws RemoteException;
-
-    /**
-     *
-     * @param code
-     * @param uid
-     * @param packageName
-     * @return
-     */
-    boolean isOperationActive(int code, int uid, String packageName);
-
-    /**
-     *
-     * @param op
-     * @param proxyPackageName
-     * @param proxyAttributionTag
-     * @param proxiedUid
-     * @param proxiedPackageName
-     * @return
-     */
-    // TODO this one might not need to be in the interface
-    boolean isProxying(int op, @NonNull String proxyPackageName,
-            @NonNull String proxyAttributionTag, int proxiedUid,
-            @NonNull String proxiedPackageName);
-
-    /**
-     *
-     * @param packageName
-     */
-    void resetPackageOpsNoHistory(@NonNull String packageName);
-
-    /**
-     *
-     * @param mode
-     * @param baseSnapshotInterval
-     * @param compressionStep
-     */
-    void setHistoryParameters(@AppOpsManager.HistoricalMode int mode,
-            long baseSnapshotInterval, int compressionStep);
-
-    /**
-     *
-     * @param offsetMillis
-     */
-    void offsetHistory(long offsetMillis);
-
-    /**
-     *
-     * @param ops
-     */
-    void addHistoricalOps(AppOpsManager.HistoricalOps ops);
-
-    /**
-     *
-     */
-    void resetHistoryParameters();
-
-    /**
-     *
-     */
-    void clearHistory();
-
-    /**
-     *
-     * @param offlineDurationMillis
-     */
-    void rebootHistory(long offlineDurationMillis);
-}
diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
index c1434e4..5114bd5 100644
--- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
@@ -59,7 +59,7 @@
     private final DelayableExecutor mExecutor;
     private final Clock mClock;
     private ActivityManagerInternal mActivityManagerInternal;
-    private AppOpsServiceImpl.Constants mConstants;
+    private AppOpsService.Constants mConstants;
 
     private SparseIntArray mUidStates = new SparseIntArray();
     private SparseIntArray mPendingUidStates = new SparseIntArray();
@@ -85,7 +85,7 @@
 
     AppOpsUidStateTrackerImpl(ActivityManagerInternal activityManagerInternal,
             Handler handler, Executor lockingExecutor, Clock clock,
-            AppOpsServiceImpl.Constants constants) {
+            AppOpsService.Constants constants) {
 
         this(activityManagerInternal, new DelayableExecutor() {
             @Override
@@ -102,7 +102,7 @@
 
     @VisibleForTesting
     AppOpsUidStateTrackerImpl(ActivityManagerInternal activityManagerInternal,
-            DelayableExecutor executor, Clock clock, AppOpsServiceImpl.Constants constants,
+            DelayableExecutor executor, Clock clock, AppOpsService.Constants constants,
             Thread executorThread) {
         mActivityManagerInternal = activityManagerInternal;
         mExecutor = executor;
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 7970269..dcc36bc 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -40,9 +40,9 @@
 import java.util.NoSuchElementException;
 
 final class AttributedOp {
-    private final @NonNull AppOpsServiceImpl mAppOpsService;
+    private final @NonNull AppOpsService mAppOpsService;
     public final @Nullable String tag;
-    public final @NonNull AppOpsServiceImpl.Op parent;
+    public final @NonNull AppOpsService.Op parent;
 
     /**
      * Last successful accesses (noteOp + finished startOp) for each uidState/opFlag combination
@@ -80,8 +80,8 @@
     // @GuardedBy("mAppOpsService")
     @Nullable ArrayMap<IBinder, InProgressStartOpEvent> mPausedInProgressEvents;
 
-    AttributedOp(@NonNull AppOpsServiceImpl appOpsService, @Nullable String tag,
-                 @NonNull AppOpsServiceImpl.Op parent) {
+    AttributedOp(@NonNull AppOpsService appOpsService, @Nullable String tag,
+            @NonNull AppOpsService.Op parent) {
         mAppOpsService = appOpsService;
         this.tag = tag;
         this.parent = parent;
@@ -131,8 +131,8 @@
 
         AppOpsManager.OpEventProxyInfo proxyInfo = null;
         if (proxyUid != Process.INVALID_UID) {
-            proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid,
-                    proxyPackageName, proxyAttributionTag);
+            proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName,
+                    proxyAttributionTag);
         }
 
         AppOpsManager.NoteOpEvent existingEvent = mAccessEvents.get(key);
@@ -238,7 +238,7 @@
         if (event == null) {
             event = mAppOpsService.mInProgressStartOpEventPool.acquire(startTime,
                     SystemClock.elapsedRealtime(), clientId, tag,
-                    PooledLambda.obtainRunnable(AppOpsServiceImpl::onClientDeath, this, clientId),
+                    PooledLambda.obtainRunnable(AppOpsService::onClientDeath, this, clientId),
                     proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags,
                     attributionFlags, attributionChainId);
             events.put(clientId, event);
@@ -251,9 +251,9 @@
         event.mNumUnfinishedStarts++;
 
         if (isStarted) {
-            mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op,
-                    parent.uid, parent.packageName, tag, uidState, flags, startTime,
-                    attributionFlags, attributionChainId);
+            mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
+                    parent.packageName, tag, uidState, flags, startTime, attributionFlags,
+                    attributionChainId);
         }
     }
 
@@ -309,8 +309,8 @@
             mAccessEvents.put(makeKey(event.getUidState(), event.getFlags()),
                     finishedEvent);
 
-            mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op,
-                    parent.uid, parent.packageName, tag, event.getUidState(),
+            mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid,
+                    parent.packageName, tag, event.getUidState(),
                     event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(),
                     event.getAttributionFlags(), event.getAttributionChainId());
 
@@ -334,13 +334,13 @@
     @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService
     private void finishPossiblyPaused(@NonNull IBinder clientId, boolean isPausing) {
         if (!isPaused()) {
-            Slog.wtf(AppOpsServiceImpl.TAG, "No ops running or paused");
+            Slog.wtf(AppOpsService.TAG, "No ops running or paused");
             return;
         }
 
         int indexOfToken = mPausedInProgressEvents.indexOfKey(clientId);
         if (indexOfToken < 0) {
-            Slog.wtf(AppOpsServiceImpl.TAG, "No op running or paused for the client");
+            Slog.wtf(AppOpsService.TAG, "No op running or paused for the client");
             return;
         } else if (isPausing) {
             // already paused
@@ -416,9 +416,9 @@
             mInProgressEvents.put(event.getClientId(), event);
             event.setStartElapsedTime(SystemClock.elapsedRealtime());
             event.setStartTime(startTime);
-            mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op,
-                    parent.uid, parent.packageName, tag, event.getUidState(), event.getFlags(),
-                    startTime, event.getAttributionFlags(), event.getAttributionChainId());
+            mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
+                    parent.packageName, tag, event.getUidState(), event.getFlags(), startTime,
+                    event.getAttributionFlags(), event.getAttributionChainId());
             if (shouldSendActive) {
                 mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
                         parent.packageName, tag, true, event.getAttributionFlags(),
@@ -503,8 +503,8 @@
                         newEvent.mNumUnfinishedStarts += numPreviousUnfinishedStarts - 1;
                     }
                 } catch (RemoteException e) {
-                    if (AppOpsServiceImpl.DEBUG) {
-                        Slog.e(AppOpsServiceImpl.TAG,
+                    if (AppOpsService.DEBUG) {
+                        Slog.e(AppOpsService.TAG,
                                 "Cannot switch to new uidState " + newState);
                     }
                 }
@@ -555,8 +555,8 @@
             ArrayMap<IBinder, InProgressStartOpEvent> ignoredEvents =
                     opToAdd.isRunning()
                             ? opToAdd.mInProgressEvents : opToAdd.mPausedInProgressEvents;
-            Slog.w(AppOpsServiceImpl.TAG, "Ignoring " + ignoredEvents.size()
-                    + " app-ops, running: " + opToAdd.isRunning());
+            Slog.w(AppOpsService.TAG, "Ignoring " + ignoredEvents.size() + " app-ops, running: "
+                    + opToAdd.isRunning());
 
             int numInProgressEvents = ignoredEvents.size();
             for (int i = 0; i < numInProgressEvents; i++) {
@@ -668,22 +668,16 @@
         /**
          * Create a new {@link InProgressStartOpEvent}.
          *
-         * @param startTime          The time {@link AppOpCheckingServiceInterface#startOperation}
-         *                          was called
-         * @param startElapsedTime   The elapsed time whe
-         *                          {@link AppOpCheckingServiceInterface#startOperation} was called
-         * @param clientId           The client id of the caller of
-         *                          {@link AppOpCheckingServiceInterface#startOperation}
+         * @param startTime          The time {@link #startOperation} was called
+         * @param startElapsedTime   The elapsed time when {@link #startOperation} was called
+         * @param clientId           The client id of the caller of {@link #startOperation}
          * @param attributionTag     The attribution tag for the operation.
          * @param onDeath            The code to execute on client death
-         * @param uidState           The uidstate of the app
-         *                          {@link AppOpCheckingServiceInterface#startOperation} was called
-         *                          for
+         * @param uidState           The uidstate of the app {@link #startOperation} was called for
          * @param attributionFlags   the attribution flags for this operation.
          * @param attributionChainId the unique id of the attribution chain this op is a part of.
-         * @param proxy              The proxy information, if
-         *                          {@link AppOpCheckingServiceInterface#startProxyOperation} was
-         *                          called
+         * @param proxy              The proxy information, if {@link #startProxyOperation} was
+         *                           called
          * @param flags              The trusted/nontrusted/self flags.
          * @throws RemoteException If the client is dying
          */
@@ -724,21 +718,15 @@
         /**
          * Reinit existing object with new state.
          *
-         * @param startTime          The time {@link AppOpCheckingServiceInterface#startOperation}
-         *                          was called
-         * @param startElapsedTime   The elapsed time when
-         *                          {@link AppOpCheckingServiceInterface#startOperation} was called
-         * @param clientId           The client id of the caller of
-         *                          {@link AppOpCheckingServiceInterface#startOperation}
+         * @param startTime          The time {@link #startOperation} was called
+         * @param startElapsedTime   The elapsed time when {@link #startOperation} was called
+         * @param clientId           The client id of the caller of {@link #startOperation}
          * @param attributionTag     The attribution tag for this operation.
          * @param onDeath            The code to execute on client death
-         * @param uidState           The uidstate of the app
-         *                          {@link AppOpCheckingServiceInterface#startOperation} was called
-         *                          for
+         * @param uidState           The uidstate of the app {@link #startOperation} was called for
          * @param flags              The flags relating to the proxy
-         * @param proxy              The proxy information, if
-         *                          {@link AppOpCheckingServiceInterface#startProxyOperation was
-         *                          called
+         * @param proxy              The proxy information, if {@link #startProxyOperation}
+         *                           was called
          * @param attributionFlags   the attribution flags for this operation.
          * @param attributionChainId the unique id of the attribution chain this op is a part of.
          * @param proxyPool          The pool to release
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index fb6511c..9b433cf 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -10003,6 +10003,7 @@
     static final int LOG_NB_EVENTS_VOLUME = 40;
     static final int LOG_NB_EVENTS_DYN_POLICY = 10;
     static final int LOG_NB_EVENTS_SPATIAL = 30;
+    static final int LOG_NB_EVENTS_SOUND_DOSE = 30;
 
     static final EventLogger
             sLifecycleLogger = new EventLogger(LOG_NB_EVENTS_LIFECYCLE,
@@ -11419,6 +11420,11 @@
             public void onStop() {
                 unregisterAudioPolicyAsync(mPolicyCallback);
             }
+
+            @Override
+            public void onCapturedContentResize(int width, int height) {
+                // Ignore resize of the captured content.
+            }
         };
         UnregisterOnStopCallback mProjectionCallback;
 
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index b920517..d30bec7 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -476,4 +476,53 @@
             }
         }
     }
+
+    static final class SoundDoseEvent extends EventLogger.Event {
+        static final int MOMENTARY_EXPOSURE = 0;
+        static final int DOSE_UPDATE = 1;
+        static final int DOSE_REPEAT_5X = 2;
+        static final int DOSE_ACCUMULATION_START = 3;
+        final int mEventType;
+        final float mFloatValue;
+        final long mLongValue;
+
+        private SoundDoseEvent(int event, float f, long l) {
+            mEventType = event;
+            mFloatValue = f;
+            mLongValue = l;
+        }
+
+        static SoundDoseEvent getMomentaryExposureEvent(float mel) {
+            return new SoundDoseEvent(MOMENTARY_EXPOSURE, mel, 0 /*ignored*/);
+        }
+
+        static SoundDoseEvent getDoseUpdateEvent(float csd, long totalDuration) {
+            return new SoundDoseEvent(DOSE_UPDATE, csd, totalDuration);
+        }
+
+        static SoundDoseEvent getDoseRepeat5xEvent() {
+            return new SoundDoseEvent(DOSE_REPEAT_5X, 0 /*ignored*/, 0 /*ignored*/);
+        }
+
+        static SoundDoseEvent getDoseAccumulationStartEvent() {
+            return new SoundDoseEvent(DOSE_ACCUMULATION_START, 0 /*ignored*/, 0 /*ignored*/);
+        }
+
+        @Override
+        public String eventToString() {
+            switch (mEventType) {
+                case MOMENTARY_EXPOSURE:
+                    return String.format("momentary exposure MEL=%.2f", mFloatValue);
+                case DOSE_UPDATE:
+                    return String.format(java.util.Locale.US,
+                            "dose update CSD=%.1f%% total duration=%d",
+                            mFloatValue * 100.0f, mLongValue);
+                case DOSE_REPEAT_5X:
+                    return "CSD reached 500%";
+                case DOSE_ACCUMULATION_START:
+                    return "CSD accumulating: RS2 entered";
+            }
+            return new StringBuilder("FIXME invalid event type:").append(mEventType).toString();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 1d25e31..c176f29 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -192,15 +192,7 @@
             synchronized (mDevicesForAttrCache) {
                 res = mDevicesForAttrCache.get(key);
                 if (res == null) {
-                    // result from AudioSystem guaranteed non-null, but could be invalid
-                    // if there is a failure to talk to APM
                     res = AudioSystem.getDevicesForAttributes(attributes, forVolume);
-                    if (res.size() > 1 && res.get(0) != null
-                            && res.get(0).getInternalType() == AudioSystem.DEVICE_NONE) {
-                        Log.e(TAG, "unable to get devices for " + attributes);
-                        // return now, do not put invalid value in cache
-                        return res;
-                    }
                     mDevicesForAttrCache.put(key, res);
                     if (DEBUG_CACHE) {
                         Log.d(TAG, mMethodNames[METHOD_GETDEVICESFORATTRIBUTES]
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 0d0de8a..5fe9ada 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -43,6 +43,8 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.audio.AudioService.AudioHandler;
 import com.android.server.audio.AudioService.ISafeHearingVolumeController;
+import com.android.server.audio.AudioServiceEvents.SoundDoseEvent;
+import com.android.server.utils.EventLogger;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -94,6 +96,9 @@
 
     private static final float CUSTOM_RS2_VALUE = 90;
 
+    private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE,
+            "CSD updates");
+
     private int mMcc = 0;
 
     final Object mSafeMediaVolumeStateLock = new Object();
@@ -147,17 +152,21 @@
         public void onMomentaryExposure(float currentMel, int deviceId) {
             Log.w(TAG, "DeviceId " + deviceId + " triggered momentary exposure with value: "
                     + currentMel);
+            mLogger.enqueue(SoundDoseEvent.getMomentaryExposureEvent(currentMel));
         }
 
         public void onNewCsdValue(float currentCsd, SoundDoseRecord[] records) {
             Log.i(TAG, "onNewCsdValue: " + currentCsd);
             mCurrentCsd = currentCsd;
             mDoseRecords.addAll(Arrays.asList(records));
+            long totalDuration = 0;
             for (SoundDoseRecord record : records) {
                 Log.i(TAG, "  new record: csd=" + record.value
                         + " averageMel=" + record.averageMel + " timestamp=" + record.timestamp
                         + " duration=" + record.duration);
+                totalDuration += record.duration;
             }
+            mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration));
         }
     };
 
@@ -400,6 +409,9 @@
         pw.print("  mMusicActiveMs="); pw.println(mMusicActiveMs);
         pw.print("  mMcc="); pw.println(mMcc);
         pw.print("  mPendingVolumeCommand="); pw.println(mPendingVolumeCommand);
+        pw.println();
+        mLogger.dump(pw);
+        pw.println();
     }
 
     /*package*/void reset() {
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 6a01042..b66120d 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -86,7 +86,8 @@
 
     @Override
     public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
-            boolean withAudio, ITunerCallback callback) throws RemoteException {
+            boolean withAudio, ITunerCallback callback, int targetSdkVersion)
+            throws RemoteException {
         if (isDebugEnabled()) {
             Slogf.d(TAG, "Opening module %d", moduleId);
         }
@@ -94,7 +95,7 @@
         if (callback == null) {
             throw new IllegalArgumentException("Callback must not be null");
         }
-        return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback);
+        return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback, targetSdkVersion);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 408fba1..8a1ba19 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -92,7 +92,8 @@
 
     @Override
     public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
-            boolean withAudio, ITunerCallback callback) throws RemoteException {
+            boolean withAudio, ITunerCallback callback, int targetSdkVersion)
+            throws RemoteException {
         if (isDebugEnabled()) {
             Slog.d(TAG, "Opening module " + moduleId);
         }
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
index 03acf72..772cd41 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
@@ -199,7 +199,8 @@
      */
     @Nullable
     public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
-            boolean withAudio, ITunerCallback callback) throws RemoteException {
+            boolean withAudio, ITunerCallback callback, int targetSdkVersion)
+            throws RemoteException {
         if (DEBUG) {
             Slogf.d(TAG, "Open AIDL radio session");
         }
@@ -222,7 +223,7 @@
             }
         }
 
-        TunerSession tunerSession = radioModule.openSession(callback);
+        TunerSession tunerSession = radioModule.openSession(callback, targetSdkVersion);
         if (legacyConfig != null) {
             tunerSession.setConfiguration(legacyConfig);
         }
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
index d90f9c4..3c0fda8 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
@@ -33,6 +33,8 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+import android.os.Build;
 import android.os.ParcelableException;
 import android.os.ServiceSpecificException;
 import android.util.ArrayMap;
@@ -62,6 +64,11 @@
         throw new UnsupportedOperationException("ConversionUtils class is noninstantiable");
     }
 
+    static boolean isAtLeastU(int targetSdkVersion) {
+        // TODO(b/261770108): Use version code for U.
+        return targetSdkVersion >= Build.VERSION_CODES.CUR_DEVELOPMENT;
+    }
+
     static RuntimeException throwOnError(RuntimeException halException, String action) {
         if (!(halException instanceof ServiceSpecificException)) {
             return new ParcelableException(new RuntimeException(
@@ -89,6 +96,27 @@
         }
     }
 
+    @RadioTuner.TunerResultType
+    static int halResultToTunerResult(int result) {
+        switch (result) {
+            case Result.OK:
+                return RadioTuner.TUNER_RESULT_OK;
+            case Result.INTERNAL_ERROR:
+                return RadioTuner.TUNER_RESULT_INTERNAL_ERROR;
+            case Result.INVALID_ARGUMENTS:
+                return RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS;
+            case Result.INVALID_STATE:
+                return RadioTuner.TUNER_RESULT_INVALID_STATE;
+            case Result.NOT_SUPPORTED:
+                return RadioTuner.TUNER_RESULT_NOT_SUPPORTED;
+            case Result.TIMEOUT:
+                return RadioTuner.TUNER_RESULT_TIMEOUT;
+            case Result.UNKNOWN_ERROR:
+            default:
+                return RadioTuner.TUNER_RESULT_UNKNOWN_ERROR;
+        }
+    }
+
     static VendorKeyValue[] vendorInfoToHalVendorKeyValues(@Nullable Map<String, String> info) {
         if (info == null) {
             return new VendorKeyValue[]{};
@@ -143,6 +171,7 @@
             case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
             case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
             case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT:
                 return ProgramSelector.PROGRAM_TYPE_DAB;
             case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
             case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
@@ -471,6 +500,60 @@
         return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed);
     }
 
+    private static boolean isNewIdentifierInU(ProgramSelector.Identifier id) {
+        return id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+    }
+
+    static boolean programSelectorMeetsSdkVersionRequirement(ProgramSelector sel,
+            int targetSdkVersion) {
+        if (isAtLeastU(targetSdkVersion)) {
+            return true;
+        }
+        if (sel.getPrimaryId().getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) {
+            return false;
+        }
+        ProgramSelector.Identifier[] secondaryIds = sel.getSecondaryIds();
+        for (int i = 0; i < secondaryIds.length; i++) {
+            if (isNewIdentifierInU(secondaryIds[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static boolean programInfoMeetsSdkVersionRequirement(RadioManager.ProgramInfo info,
+            int targetSdkVersion) {
+        if (isAtLeastU(targetSdkVersion)) {
+            return true;
+        }
+        if (!programSelectorMeetsSdkVersionRequirement(info.getSelector(), targetSdkVersion)) {
+            return false;
+        }
+        if (isNewIdentifierInU(info.getLogicallyTunedTo())
+                || isNewIdentifierInU(info.getPhysicallyTunedTo())) {
+            return false;
+        }
+        Iterator<ProgramSelector.Identifier> relatedContentIt = info.getRelatedContent().iterator();
+        while (relatedContentIt.hasNext()) {
+            if (isNewIdentifierInU(relatedContentIt.next())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static ProgramList.Chunk convertChunkToTargetSdkVersion(ProgramList.Chunk chunk,
+            int targetSdkVersion) {
+        if (isAtLeastU(targetSdkVersion)) {
+            return chunk;
+        }
+        Set<RadioManager.ProgramInfo> modified = chunk.getModified();
+        modified.removeIf(info -> !programInfoMeetsSdkVersionRequirement(info, targetSdkVersion));
+        Set<ProgramSelector.Identifier> removed = chunk.getRemoved();
+        removed.removeIf(id -> isNewIdentifierInU(id));
+        return new ProgramList.Chunk(chunk.isPurge(), chunk.isComplete(), modified, removed);
+    }
+
     public static android.hardware.radio.Announcement announcementFromHalAnnouncement(
             Announcement hwAnnouncement) {
         return new android.hardware.radio.Announcement(
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
index e956a9c..6193f23 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -100,7 +100,16 @@
                 synchronized (mLock) {
                     android.hardware.radio.ProgramSelector csel =
                             ConversionUtils.programSelectorFromHalProgramSelector(programSelector);
-                    fanoutAidlCallbackLocked(cb -> cb.onTuneFailed(result, csel));
+                    int tunerResult = ConversionUtils.halResultToTunerResult(result);
+                    fanoutAidlCallbackLocked((cb, sdkVersion) -> {
+                        if (csel != null && !ConversionUtils
+                                .programSelectorMeetsSdkVersionRequirement(csel, sdkVersion)) {
+                            Slogf.e(TAG, "onTuneFailed: cannot send program selector "
+                                    + "requiring higher target SDK version");
+                            return;
+                        }
+                        cb.onTuneFailed(tunerResult, csel);
+                    });
                 }
             });
         }
@@ -112,7 +121,13 @@
                     mCurrentProgramInfo =
                             ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo);
                     RadioManager.ProgramInfo currentProgramInfo = mCurrentProgramInfo;
-                    fanoutAidlCallbackLocked(cb -> {
+                    fanoutAidlCallbackLocked((cb, sdkVersion) -> {
+                        if (!ConversionUtils.programInfoMeetsSdkVersionRequirement(
+                                currentProgramInfo, sdkVersion)) {
+                            Slogf.e(TAG, "onCurrentProgramInfoChanged: cannot send "
+                                    + "program info requiring higher target SDK version");
+                            return;
+                        }
                         cb.onCurrentProgramInfoChanged(currentProgramInfo);
                     });
                 }
@@ -139,7 +154,7 @@
             fireLater(() -> {
                 synchronized (mLock) {
                     mAntennaConnected = connected;
-                    fanoutAidlCallbackLocked(cb -> cb.onAntennaState(connected));
+                    fanoutAidlCallbackLocked((cb, sdkVersion) -> cb.onAntennaState(connected));
                 }
             });
         }
@@ -147,8 +162,11 @@
         @Override
         public void onConfigFlagUpdated(int flag, boolean value) {
             fireLater(() -> {
-                // TODO(b/243853343): implement config flag update method in
-                //  android.hardware.radio.ITunerCallback
+                synchronized (mLock) {
+                    fanoutAidlCallbackLocked((cb, sdkVersion) -> {
+                        cb.onConfigFlagUpdated(flag, value);
+                    });
+                }
             });
         }
 
@@ -158,7 +176,9 @@
                 synchronized (mLock) {
                     Map<String, String> cparam =
                             ConversionUtils.vendorInfoFromHalVendorKeyValues(parameters);
-                    fanoutAidlCallbackLocked(cb -> cb.onParametersUpdated(cparam));
+                    fanoutAidlCallbackLocked((cb, sdkVersion) -> {
+                        cb.onParametersUpdated(cparam);
+                    });
                 }
             });
         }
@@ -222,14 +242,14 @@
         mService.setTunerCallback(mHalTunerCallback);
     }
 
-    TunerSession openSession(android.hardware.radio.ITunerCallback userCb)
+    TunerSession openSession(android.hardware.radio.ITunerCallback userCb, int targetSdkVersion)
             throws RemoteException {
         mLogger.logRadioEvent("Open TunerSession");
         TunerSession tunerSession;
         Boolean antennaConnected;
         RadioManager.ProgramInfo currentProgramInfo;
         synchronized (mLock) {
-            tunerSession = new TunerSession(this, mService, userCb);
+            tunerSession = new TunerSession(this, mService, userCb, targetSdkVersion);
             mAidlTunerSessions.add(tunerSession);
             antennaConnected = mAntennaConnected;
             currentProgramInfo = mCurrentProgramInfo;
@@ -382,7 +402,8 @@
     }
 
     interface AidlCallbackRunnable {
-        void run(android.hardware.radio.ITunerCallback callback) throws RemoteException;
+        void run(android.hardware.radio.ITunerCallback callback, int targetSdkVersion)
+                throws RemoteException;
     }
 
     // Invokes runnable with each TunerSession currently open.
@@ -399,7 +420,8 @@
         List<TunerSession> deadSessions = null;
         for (int i = 0; i < mAidlTunerSessions.size(); i++) {
             try {
-                runnable.run(mAidlTunerSessions.valueAt(i).mCallback);
+                runnable.run(mAidlTunerSessions.valueAt(i).mCallback,
+                        mAidlTunerSessions.valueAt(i).getTargetSdkVersion());
             } catch (DeadObjectException ex) {
                 // The other side died without calling close(), so just purge it from our records.
                 Slogf.e(TAG, "Removing dead TunerSession");
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
index 1ce4044..d700ed0 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
@@ -21,7 +21,6 @@
 import android.hardware.broadcastradio.ConfigFlag;
 import android.hardware.broadcastradio.IBroadcastRadio;
 import android.hardware.radio.ITuner;
-import android.hardware.radio.ITunerCallback;
 import android.hardware.radio.ProgramList;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
@@ -47,6 +46,7 @@
     private final RadioLogger mLogger;
     private final RadioModule mModule;
     final android.hardware.radio.ITunerCallback mCallback;
+    private final int mTargetSdkVersion;
     private final IBroadcastRadio mService;
 
     @GuardedBy("mLock")
@@ -61,10 +61,11 @@
     private RadioManager.BandConfig mPlaceHolderConfig;
 
     TunerSession(RadioModule radioModule, IBroadcastRadio service,
-            android.hardware.radio.ITunerCallback callback) {
+            android.hardware.radio.ITunerCallback callback, int targetSdkVersion) {
         mModule = Objects.requireNonNull(radioModule, "radioModule cannot be null");
         mService = Objects.requireNonNull(service, "service cannot be null");
         mCallback = Objects.requireNonNull(callback, "callback cannot be null");
+        mTargetSdkVersion = targetSdkVersion;
         mLogger = new RadioLogger(TAG, TUNER_EVENT_LOGGER_QUEUE_SIZE);
     }
 
@@ -129,7 +130,7 @@
             mPlaceHolderConfig = Objects.requireNonNull(config, "config cannot be null");
         }
         Slogf.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL AIDL");
-        mModule.fanoutAidlCallback(cb -> cb.onConfigurationChanged(config));
+        mModule.fanoutAidlCallback((cb, sdkVersion) -> cb.onConfigurationChanged(config));
     }
 
     @Override
@@ -178,8 +179,8 @@
     }
 
     @Override
-    public void scan(boolean directionDown, boolean skipSubChannel) throws RemoteException {
-        mLogger.logRadioEvent("Scan with direction %s, skipSubChannel? %s",
+    public void seek(boolean directionDown, boolean skipSubChannel) throws RemoteException {
+        mLogger.logRadioEvent("Seek with direction %s, skipSubChannel? %s",
                 directionDown ? "down" : "up", skipSubChannel ? "yes" : "no");
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.w(TAG, "Cannot scan on AIDL HAL client from non-current user");
@@ -232,8 +233,7 @@
 
     @Override
     public void cancelAnnouncement() {
-        // TODO(b/244485175): deperacte cancelAnnouncement
-        Slogf.i(TAG, "Announcements control doesn't involve cancelling at the HAL level in AIDL");
+        Slogf.w(TAG, "Announcements control doesn't involve cancelling at the HAL level in AIDL");
     }
 
     @Override
@@ -244,12 +244,14 @@
 
     @Override
     public boolean startBackgroundScan() {
-        Slogf.i(TAG, "Explicit background scan trigger is not supported with HAL AIDL");
+        Slogf.w(TAG, "Explicit background scan trigger is not supported with HAL AIDL");
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.w(TAG, "Cannot start background scan on AIDL HAL client from non-current user");
             return false;
         }
-        mModule.fanoutAidlCallback(ITunerCallback::onBackgroundScanComplete);
+        mModule.fanoutAidlCallback((cb, sdkVersion) -> {
+            cb.onBackgroundScanComplete();
+        });
         return true;
     }
 
@@ -277,6 +279,10 @@
         mModule.onTunerSessionProgramListFilterChanged(this);
     }
 
+    int getTargetSdkVersion() {
+        return mTargetSdkVersion;
+    }
+
     ProgramList.Filter getProgramListFilter() {
         synchronized (mLock) {
             return mProgramInfoCache == null ? null : mProgramInfoCache.getFilter();
@@ -312,7 +318,14 @@
         }
         for (int i = 0; i < chunks.size(); i++) {
             try {
-                mCallback.onProgramListUpdated(chunks.get(i));
+                if (!ConversionUtils.isAtLeastU(getTargetSdkVersion())) {
+                    ProgramList.Chunk downgradedChunk =
+                            ConversionUtils.convertChunkToTargetSdkVersion(chunks.get(i),
+                                    getTargetSdkVersion());
+                    mCallback.onProgramListUpdated(downgradedChunk);
+                } else {
+                    mCallback.onProgramListUpdated(chunks.get(i));
+                }
             } catch (RemoteException ex) {
                 Slogf.w(TAG, ex, "mCallback.onProgramListUpdated() failed");
             }
diff --git a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
index ed8a37a..8e5f6b5 100644
--- a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
+++ b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
@@ -189,9 +189,9 @@
     }
 
     @Override
-    public void scan(boolean directionDown, boolean skipSubChannel) {
+    public void seek(boolean directionDown, boolean skipSubChannel) {
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
-            Slogf.w(TAG, "Cannot scan on HAL 1.x client from non-current user");
+            Slogf.w(TAG, "Cannot seek on HAL 1.x client from non-current user");
             return;
         }
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
index 0cc3833..aa43b75 100644
--- a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
+++ b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
@@ -174,8 +174,13 @@
     }
 
     @Override
+    public void onConfigFlagUpdated(int flag, boolean value) {
+        Slog.w(TAG, "Not applicable for HAL 1.x");
+    }
+
+    @Override
     public void onParametersUpdated(Map<String, String> parameters) {
-        Slog.e(TAG, "Not applicable for HAL 1.x");
+        Slog.w(TAG, "Not applicable for HAL 1.x");
     }
 
     @Override
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
index 984bf51..1e31f20 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -169,7 +169,7 @@
     }
 
     public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
-        boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
+            boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
         Slog.v(TAG, "Open HIDL 2.0 session with module id " + moduleId);
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.e(TAG, "Cannot open tuner on HAL 2.0 client for non-current user");
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
index 3daf1db..98a450f 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
@@ -36,6 +36,7 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
 import android.os.ParcelableException;
 import android.util.Slog;
 
@@ -81,6 +82,27 @@
         }
     }
 
+    @RadioTuner.TunerResultType
+    static int halResultToTunerResult(int result) {
+        switch (result) {
+            case Result.OK:
+                return RadioTuner.TUNER_RESULT_OK;
+            case Result.INTERNAL_ERROR:
+                return RadioTuner.TUNER_RESULT_INTERNAL_ERROR;
+            case Result.INVALID_ARGUMENTS:
+                return RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS;
+            case Result.INVALID_STATE:
+                return RadioTuner.TUNER_RESULT_INVALID_STATE;
+            case Result.NOT_SUPPORTED:
+                return RadioTuner.TUNER_RESULT_NOT_SUPPORTED;
+            case Result.TIMEOUT:
+                return RadioTuner.TUNER_RESULT_TIMEOUT;
+            case Result.UNKNOWN_ERROR:
+            default:
+                return RadioTuner.TUNER_RESULT_UNKNOWN_ERROR;
+        }
+    }
+
     static @NonNull ArrayList<VendorKeyValue>
     vendorInfoToHal(@Nullable Map<String, String> info) {
         if (info == null) return new ArrayList<>();
@@ -130,6 +152,7 @@
             case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
             case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
             case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT:
                 return ProgramSelector.PROGRAM_TYPE_DAB;
             case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
             case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index 0ea5f0f..59a8154 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -87,8 +87,9 @@
             fireLater(() -> {
                 android.hardware.radio.ProgramSelector csel =
                         Convert.programSelectorFromHal(programSelector);
+                int tunerResult = Convert.halResultToTunerResult(result);
                 synchronized (mLock) {
-                    fanoutAidlCallbackLocked(cb -> cb.onTuneFailed(result, csel));
+                    fanoutAidlCallbackLocked(cb -> cb.onTuneFailed(tunerResult, csel));
                 }
             });
         }
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
index 7afee27..204b964 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
@@ -171,8 +171,8 @@
     }
 
     @Override
-    public void scan(boolean directionDown, boolean skipSubChannel) throws RemoteException {
-        mEventLogger.logRadioEvent("Scan with direction %s, skipSubChannel? %s",
+    public void seek(boolean directionDown, boolean skipSubChannel) throws RemoteException {
+        mEventLogger.logRadioEvent("Seek with direction %s, skipSubChannel? %s",
                 directionDown ? "down" : "up", skipSubChannel ? "yes" : "no");
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.w(TAG, "Cannot scan on HAL 2.0 client from non-current user");
@@ -214,7 +214,7 @@
 
     @Override
     public void cancelAnnouncement() {
-        Slog.i(TAG, "Announcements control doesn't involve cancelling at the HAL level in HAL 2.0");
+        Slog.w(TAG, "Announcements control doesn't involve cancelling at the HAL level in HAL 2.0");
     }
 
     @Override
@@ -225,7 +225,7 @@
 
     @Override
     public boolean startBackgroundScan() {
-        Slog.i(TAG, "Explicit background scan trigger is not supported with HAL 2.0");
+        Slog.w(TAG, "Explicit background scan trigger is not supported with HAL 2.0");
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.w(TAG,
                     "Cannot start background scan on HAL 2.0 client from non-current user");
diff --git a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
index ab3b250..dce1c96 100644
--- a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
+++ b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
@@ -29,6 +29,7 @@
 import java.io.EOFException;
 import java.io.FileDescriptor;
 import java.io.InterruptedIOException;
+import java.net.ProtocolException;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -39,12 +40,16 @@
 // write contents of the host system's clipboard.
 class EmulatorClipboardMonitor implements Consumer<ClipData> {
     private static final String TAG = "EmulatorClipboardMonitor";
+
     private static final String PIPE_NAME = "pipe:clipboard";
     private static final int HOST_PORT = 5000;
-    private final Thread mHostMonitorThread;
+
     private static final boolean LOG_CLIBOARD_ACCESS =
             SystemProperties.getBoolean("ro.boot.qemu.log_clipboard_access", false);
+    private static final int MAX_CLIPBOARD_BYTES = 128 << 20;
+
     private FileDescriptor mPipe = null;
+    private final Thread mHostMonitorThread;
 
     private static byte[] createOpenHandshake() {
         // String.getBytes doesn't include the null terminator,
@@ -97,8 +102,8 @@
         return fd;
     }
 
-    private static byte[] receiveMessage(final FileDescriptor fd) throws ErrnoException,
-            InterruptedIOException, EOFException {
+    private byte[] receiveMessage(final FileDescriptor fd) throws ErrnoException,
+            InterruptedIOException, EOFException, ProtocolException {
         final byte[] lengthBits = new byte[4];
         readFully(fd, lengthBits, 0, lengthBits.length);
 
@@ -106,6 +111,10 @@
         bb.order(ByteOrder.LITTLE_ENDIAN);
         final int msgLen = bb.getInt();
 
+        if (msgLen <= 0 || msgLen > MAX_CLIPBOARD_BYTES) {
+            throw new ProtocolException("Clipboard message length: " + msgLen + " out of bounds.");
+        }
+
         final byte[] msg = new byte[msgLen];
         readFully(fd, msg, 0, msg.length);
 
@@ -150,7 +159,8 @@
                     }
                     setAndroidClipboard.accept(clip);
                 } catch (ErrnoException | EOFException | InterruptedIOException
-                         | InterruptedException e) {
+                         | InterruptedException | ProtocolException | OutOfMemoryError e) {
+                    Slog.w(TAG, "Failure to read from host clipboard", e);
                     setPipeFD(null);
 
                     try {
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 218be9d..09bec5e 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -36,7 +36,6 @@
         void onVirtualDisplayRemoved(int displayId);
     }
 
-
     /** Interface to listen to the changes on the list of app UIDs running on any virtual device. */
     public interface AppsOnVirtualDeviceListener {
         /** Notifies that running apps on any virtual device has changed */
@@ -72,6 +71,25 @@
     public abstract boolean isValidVirtualDevice(IVirtualDevice virtualDevice);
 
     /**
+     * Gets the owner uid for a deviceId.
+     *
+     * @param deviceId which device we're asking about
+     * @return the uid of the app which created and owns the VirtualDevice with the given deviceId,
+     * or {@link android.os.Process#INVALID_UID} if no such device exists.
+     */
+    public abstract int getDeviceOwnerUid(int deviceId);
+
+    /**
+     * Finds VirtualDevices where an app is running.
+     *
+     * @param uid - the app's uid
+     * @return a set of id's of VirtualDevices where the app with the given uid is running.
+     * *Note* this only checks VirtualDevices, and does not include information about whether
+     * the app is running on the default device or not.
+     */
+    public abstract @NonNull Set<Integer> getDeviceIdsForUid(int uid);
+
+    /**
      * Notifies that a virtual display is created.
      *
      * @param displayId The display id of the created virtual display.
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 4fcde97..19dbee7 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -646,7 +646,10 @@
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
                 .setTransportInfo(new VpnTransportInfo(
-                        VpnManager.TYPE_VPN_NONE, null /* sessionId */, false /* bypassable */))
+                        VpnManager.TYPE_VPN_NONE,
+                        null /* sessionId */,
+                        false /* bypassable */,
+                        false /* longLivedTcpConnectionsExpensive */))
                 .build();
 
         loadAlwaysOnPackage();
@@ -711,7 +714,10 @@
         mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities)
                 .setUids(null)
                 .setTransportInfo(new VpnTransportInfo(
-                        VpnManager.TYPE_VPN_NONE, null /* sessionId */, false /* bypassable */))
+                        VpnManager.TYPE_VPN_NONE,
+                        null /* sessionId */,
+                        false /* bypassable */,
+                        false /* longLivedTcpConnectionsExpensive */))
                 .build();
     }
 
@@ -1570,7 +1576,8 @@
                 mConfig.allowedApplications, mConfig.disallowedApplications));
 
         capsBuilder.setTransportInfo(
-                new VpnTransportInfo(getActiveVpnType(), mConfig.session, mConfig.allowBypass));
+                new VpnTransportInfo(getActiveVpnType(), mConfig.session, mConfig.allowBypass,
+                        false /* longLivedTcpConnectionsExpensive */));
 
         // Only apps targeting Q and above can explicitly declare themselves as metered.
         // These VPNs are assumed metered unless they state otherwise.
diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java
index 372bc8a..a4bd6a6 100644
--- a/services/core/java/com/android/server/display/ColorFade.java
+++ b/services/core/java/com/android/server/display/ColorFade.java
@@ -16,7 +16,7 @@
 
 package com.android.server.display;
 
-import static com.android.server.wm.utils.RotationAnimationUtils.hasProtectedContent;
+import static com.android.internal.policy.TransitionAnimation.hasProtectedContent;
 
 import android.content.Context;
 import android.graphics.BLASTBufferQueue;
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index c700ccb..329e3ca 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1852,15 +1852,7 @@
         Settings.Global.putInt(mContext.getContentResolver(),
                 Settings.Global.USER_PREFERRED_RESOLUTION_WIDTH, resolutionWidth);
         mDisplayDeviceRepo.forEachLocked((DisplayDevice device) -> {
-            // If there is a display specific mode, don't override that
-            final Point deviceUserPreferredResolution =
-                    mPersistentDataStore.getUserPreferredResolution(device);
-            final float deviceRefreshRate =
-                    mPersistentDataStore.getUserPreferredRefreshRate(device);
-            if (!isValidResolution(deviceUserPreferredResolution)
-                    && !isValidRefreshRate(deviceRefreshRate)) {
-                device.setUserPreferredDisplayModeLocked(mode);
-            }
+            device.setUserPreferredDisplayModeLocked(mode);
         });
     }
 
@@ -3723,44 +3715,21 @@
         @Override
         public Set<DisplayInfo> getPossibleDisplayInfo(int displayId) {
             synchronized (mSyncRoot) {
-                // Retrieve the group associated with this display id.
-                final int displayGroupId =
-                        mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(displayId);
-                if (displayGroupId == Display.INVALID_DISPLAY_GROUP) {
-                    Slog.w(TAG,
-                            "Can't get possible display info since display group for " + displayId
-                                    + " does not exist");
-                    return new ArraySet<>();
-                }
-
-                // Assume any display in this group can be swapped out for the given display id.
                 Set<DisplayInfo> possibleInfo = new ArraySet<>();
-                final DisplayGroup group = mLogicalDisplayMapper.getDisplayGroupLocked(
-                        displayGroupId);
-                for (int i = 0; i < group.getSizeLocked(); i++) {
-                    final int id = group.getIdLocked(i);
-                    final LogicalDisplay logical = mLogicalDisplayMapper.getDisplayLocked(id);
-                    if (logical == null) {
-                        Slog.w(TAG,
-                                "Can't get possible display info since logical display for "
-                                        + "display id " + id + " does not exist, as part of group "
-                                        + displayGroupId);
-                    } else {
-                        possibleInfo.add(logical.getDisplayInfoLocked());
-                    }
-                }
-
-                // For the supported device states, retrieve the DisplayInfos for the logical
-                // display layout.
+                // For each of supported device states, retrieve the display layout of that state,
+                // and return all of the DisplayInfos (one per state) for the given display id.
                 if (mDeviceStateManager == null) {
                     Slog.w(TAG, "Can't get supported states since DeviceStateManager not ready");
-                } else {
-                    final int[] supportedStates =
-                            mDeviceStateManager.getSupportedStateIdentifiers();
-                    for (int state : supportedStates) {
-                        possibleInfo.addAll(
-                                mLogicalDisplayMapper.getDisplayInfoForStateLocked(state, displayId,
-                                        displayGroupId));
+                    return possibleInfo;
+                }
+                final int[] supportedStates =
+                        mDeviceStateManager.getSupportedStateIdentifiers();
+                DisplayInfo displayInfo;
+                for (int state : supportedStates) {
+                    displayInfo = mLogicalDisplayMapper.getDisplayInfoForStateLocked(state,
+                            displayId);
+                    if (displayInfo != null) {
+                        possibleInfo.add(displayInfo);
                     }
                 }
                 return possibleInfo;
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 9d47892..75415cd 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -453,6 +453,8 @@
     // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set.
     private float mTemporaryAutoBrightnessAdjustment;
 
+    private boolean mUseAutoBrightness;
+
     private boolean mIsRbcActive;
 
     // Whether there's a callback to tell listeners the display has changed scheduled to run. When
@@ -683,6 +685,7 @@
     @Override
     public void onSwitchUser(@UserIdInt int newUserId) {
         handleSettingsChange(true /* userSwitch */);
+        handleBrightnessModeChange();
         if (mBrightnessTracker != null) {
             mBrightnessTracker.onSwitchUser(newUserId);
         }
@@ -930,6 +933,10 @@
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ),
                 false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE),
+                false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
+        handleBrightnessModeChange();
     }
 
     private void setUpAutoBrightness(Resources resources, Handler handler) {
@@ -1335,11 +1342,11 @@
 
         final boolean autoBrightnessEnabledInDoze =
                 mAllowAutoBrightnessWhileDozingConfig && Display.isDozeState(state);
-        final boolean autoBrightnessEnabled = mPowerRequest.useAutoBrightness
+        final boolean autoBrightnessEnabled = mUseAutoBrightness
                 && (state == Display.STATE_ON || autoBrightnessEnabledInDoze)
                 && Float.isNaN(brightnessState)
                 && mAutomaticBrightnessController != null;
-        final boolean autoBrightnessDisabledDueToDisplayOff = mPowerRequest.useAutoBrightness
+        final boolean autoBrightnessDisabledDueToDisplayOff = mUseAutoBrightness
                 && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze);
         final int autoBrightnessState = autoBrightnessEnabled
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
@@ -1691,7 +1698,7 @@
                 || brightnessAdjustmentFlags != 0) {
             float lastBrightness = mLastBrightnessEvent.getBrightness();
             mTempBrightnessEvent.setInitialBrightness(lastBrightness);
-            mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness);
+            mTempBrightnessEvent.setAutomaticBrightnessEnabled(mUseAutoBrightness);
             mLastBrightnessEvent.copyFrom(mTempBrightnessEvent);
             BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent);
             // Adjustment flags (and user-set flag) only get added after the equality checks since
@@ -2341,6 +2348,18 @@
         sendUpdatePowerState();
     }
 
+    private void handleBrightnessModeChange() {
+        final int screenBrightnessModeSetting = Settings.System.getIntForUser(
+                mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
+        mHandler.post(() -> {
+            mUseAutoBrightness = screenBrightnessModeSetting
+                    == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC;
+            updatePowerState();
+        });
+    }
+
     private float getAutoBrightnessAdjustmentSetting() {
         final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(),
                 Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT);
@@ -2425,7 +2444,7 @@
     private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated,
             boolean hadUserDataPoint) {
         final float brightnessInNits = convertToNits(brightness);
-        if (mPowerRequest.useAutoBrightness && brightnessInNits >= 0.0f
+        if (mUseAutoBrightness && brightnessInNits >= 0.0f
                 && mAutomaticBrightnessController != null && mBrightnessTracker != null) {
             // We only want to track changes on devices that can actually map the display backlight
             // values into a physical brightness unit since the value provided by the API is in
@@ -2897,7 +2916,11 @@
 
         @Override
         public void onChange(boolean selfChange, Uri uri) {
-            handleSettingsChange(false /* userSwitch */);
+            if (uri.equals(Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE))) {
+                handleBrightnessModeChange();
+            } else {
+                handleSettingsChange(false /* userSwitch */);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 346b340..111caefa 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -71,6 +71,7 @@
 import com.android.server.display.brightness.DisplayBrightnessController;
 import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
 import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
+import com.android.server.display.state.DisplayStateController;
 import com.android.server.display.utils.SensorUtils;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory;
@@ -346,6 +347,9 @@
     // Tracks and manages the proximity state of the associated display.
     private final DisplayPowerProximityStateController mDisplayPowerProximityStateController;
 
+    // Tracks and manages the display state of the associated display.
+    private final DisplayStateController mDisplayStateController;
+
     // A record of state for skipping brightness ramps.
     private int mSkipRampState = RAMP_STATE_SKIP_NONE;
 
@@ -396,6 +400,8 @@
     // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set.
     private float mTemporaryAutoBrightnessAdjustment;
 
+    private boolean mUseAutoBrightness;
+
     private boolean mIsRbcActive;
 
     // Animators.
@@ -434,6 +440,7 @@
         mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
                 mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
                 () -> updatePowerState(), mDisplayId, mSensorManager);
+        mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
         mTag = "DisplayPowerController2[" + mDisplayId + "]";
 
         mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
@@ -600,6 +607,7 @@
     @Override
     public void onSwitchUser(@UserIdInt int newUserId) {
         handleSettingsChange(true /* userSwitch */);
+        handleBrightnessModeChange();
         if (mBrightnessTracker != null) {
             mBrightnessTracker.onSwitchUser(newUserId);
         }
@@ -842,6 +850,10 @@
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ),
                 false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE),
+                false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
+        handleBrightnessModeChange();
     }
 
     private void setUpAutoBrightness(Resources resources, Handler handler) {
@@ -1121,39 +1133,8 @@
             mustNotify = !mDisplayReadyLocked;
         }
 
-        // Compute the basic display state using the policy.
-        // We might override this below based on other factors.
-        // Initialise brightness as invalid.
-        int state;
-        boolean performScreenOffTransition = false;
-        switch (mPowerRequest.policy) {
-            case DisplayPowerRequest.POLICY_OFF:
-                state = Display.STATE_OFF;
-                performScreenOffTransition = true;
-                break;
-            case DisplayPowerRequest.POLICY_DOZE:
-                if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
-                    state = mPowerRequest.dozeScreenState;
-                } else {
-                    state = Display.STATE_DOZE;
-                }
-                break;
-            case DisplayPowerRequest.POLICY_DIM:
-            case DisplayPowerRequest.POLICY_BRIGHT:
-            default:
-                state = Display.STATE_ON;
-                break;
-        }
-        assert (state != Display.STATE_UNKNOWN);
-
-        mDisplayPowerProximityStateController.updateProximityState(mPowerRequest, state);
-
-        if (!mIsEnabled
-                || mIsInTransition
-                || mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
-            state = Display.STATE_OFF;
-        }
-
+        int state = mDisplayStateController
+                .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition);
         // Initialize things the first time the power state is changed.
         if (mustInitialize) {
             initialize(state);
@@ -1163,7 +1144,7 @@
         // The transition may be deferred, so after this point we will use the
         // actual state instead of the desired one.
         final int oldState = mPowerState.getScreenState();
-        animateScreenStateChange(state, performScreenOffTransition);
+        animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition());
         state = mPowerState.getScreenState();
 
         DisplayBrightnessState displayBrightnessState = mDisplayBrightnessController
@@ -1174,11 +1155,11 @@
         final boolean autoBrightnessEnabledInDoze =
                 mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()
                         && Display.isDozeState(state);
-        final boolean autoBrightnessEnabled = mPowerRequest.useAutoBrightness
+        final boolean autoBrightnessEnabled = mUseAutoBrightness
                 && (state == Display.STATE_ON || autoBrightnessEnabledInDoze)
                 && Float.isNaN(brightnessState)
                 && mAutomaticBrightnessController != null;
-        final boolean autoBrightnessDisabledDueToDisplayOff = mPowerRequest.useAutoBrightness
+        final boolean autoBrightnessDisabledDueToDisplayOff = mUseAutoBrightness
                 && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze);
         final int autoBrightnessState = autoBrightnessEnabled
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
@@ -1510,7 +1491,7 @@
                 || brightnessAdjustmentFlags != 0) {
             float lastBrightness = mLastBrightnessEvent.getBrightness();
             mTempBrightnessEvent.setInitialBrightness(lastBrightness);
-            mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness);
+            mTempBrightnessEvent.setAutomaticBrightnessEnabled(mUseAutoBrightness);
             mLastBrightnessEvent.copyFrom(mTempBrightnessEvent);
             BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent);
             // Adjustment flags (and user-set flag) only get added after the equality checks since
@@ -2045,6 +2026,18 @@
         sendUpdatePowerState();
     }
 
+    private void handleBrightnessModeChange() {
+        final int screenBrightnessModeSetting = Settings.System.getIntForUser(
+                mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
+        mHandler.post(() -> {
+            mUseAutoBrightness = screenBrightnessModeSetting
+                    == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC;
+            updatePowerState();
+        });
+    }
+
     private float getAutoBrightnessAdjustmentSetting() {
         final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(),
                 Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT);
@@ -2131,7 +2124,7 @@
     private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated,
             boolean hadUserDataPoint) {
         final float brightnessInNits = convertToNits(brightness);
-        if (mPowerRequest.useAutoBrightness && brightnessInNits >= 0.0f
+        if (mUseAutoBrightness && brightnessInNits >= 0.0f
                 && mAutomaticBrightnessController != null && mBrightnessTracker != null) {
             // We only want to track changes on devices that can actually map the display backlight
             // values into a physical brightness unit since the value provided by the API is in
@@ -2274,10 +2267,6 @@
         if (mDisplayBrightnessController != null) {
             mDisplayBrightnessController.dump(pw);
         }
-
-        if (mDisplayPowerProximityStateController != null) {
-            mDisplayPowerProximityStateController.dumpLocal(pw);
-        }
     }
 
 
@@ -2499,7 +2488,11 @@
 
         @Override
         public void onChange(boolean selfChange, Uri uri) {
-            handleSettingsChange(false /* userSwitch */);
+            if (uri.equals(Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE))) {
+                handleBrightnessModeChange();
+            } else {
+                handleSettingsChange(false /* userSwitch */);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index c7b27de..ad426b5 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -66,7 +66,6 @@
  */
 final class LogicalDisplay {
     private static final String TAG = "LogicalDisplay";
-
     // The layer stack we use when the display has been blanked to prevent any
     // of its content from appearing.
     private static final int BLANK_LAYER_STACK = -1;
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 80f47a1..d7983ae 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -19,6 +19,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.devicestate.DeviceStateManager;
 import android.os.Handler;
@@ -29,7 +30,6 @@
 import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -45,7 +45,6 @@
 
 import java.io.PrintWriter;
 import java.util.Arrays;
-import java.util.Set;
 import java.util.function.Consumer;
 
 /**
@@ -324,58 +323,44 @@
     }
 
     /**
-     * Returns the set of {@link DisplayInfo} for this device state, only fetching the info that is
-     * part of the same display group as the provided display id. The DisplayInfo represent the
-     * logical display layouts possible for the given device state.
+     * Returns the {@link DisplayInfo} for this device state, indicated by the given display id. The
+     * DisplayInfo represents the attributes of the indicated display in the layout associated with
+     * this state. This is used to get display information for various displays in various states;
+     * e.g. to help apps preload resources for the possible display states.
      *
      * @param deviceState the state to query possible layouts for
-     * @param displayId   the display id to apply to all displays within the group
-     * @param groupId     the display group to filter display info for. Must be the same group as
-     *                    the display with the provided display id.
+     * @param displayId   the display id to retrieve
+     * @return {@code null} if no corresponding {@link DisplayInfo} could be found, or the
+     * {@link DisplayInfo} with a matching display id.
      */
-    public Set<DisplayInfo> getDisplayInfoForStateLocked(int deviceState, int displayId,
-            int groupId) {
-        Set<DisplayInfo> displayInfos = new ArraySet<>();
+    @Nullable
+    public DisplayInfo getDisplayInfoForStateLocked(int deviceState, int displayId) {
+        // Retrieve the layout for this particular state.
         final Layout layout = mDeviceStateToLayoutMap.get(deviceState);
-        final int layoutSize = layout.size();
-        for (int i = 0; i < layoutSize; i++) {
-            Layout.Display displayLayout = layout.getAt(i);
-            if (displayLayout == null) {
-                continue;
-            }
-
-            // If the underlying display-device we want to use for this display
-            // doesn't exist, then skip it. This can happen at startup as display-devices
-            // trickle in one at a time. When the new display finally shows up, the layout is
-            // recalculated so that the display is properly added to the current layout.
-            final DisplayAddress address = displayLayout.getAddress();
-            final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(address);
-            if (device == null) {
-                Slog.w(TAG, "The display device (" + address + "), is not available"
-                        + " for the display state " + deviceState);
-                continue;
-            }
-
-            // Find or create the LogicalDisplay to map the DisplayDevice to.
-            final int logicalDisplayId = displayLayout.getLogicalDisplayId();
-            final LogicalDisplay logicalDisplay = getDisplayLocked(logicalDisplayId);
-            if (logicalDisplay == null) {
-                Slog.w(TAG, "The logical display (" + address + "), is not available"
-                        + " for the display state " + deviceState);
-                continue;
-            }
-            final DisplayInfo temp = logicalDisplay.getDisplayInfoLocked();
-            DisplayInfo displayInfo = new DisplayInfo(temp);
-            if (displayInfo.displayGroupId != groupId) {
-                // Ignore any displays not in the provided group.
-                continue;
-            }
-            // A display in the same group can be swapped out at any point, so set the display id
-            // for all results to the provided display id.
-            displayInfo.displayId = displayId;
-            displayInfos.add(displayInfo);
+        if (layout == null) {
+            return null;
         }
-        return displayInfos;
+        // Retrieve the details of the given display within this layout.
+        Layout.Display display = layout.getById(displayId);
+        if (display == null) {
+            return null;
+        }
+        // Retrieve the display info for the display that matches the display id.
+        final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(display.getAddress());
+        if (device == null) {
+            Slog.w(TAG, "The display device (" + display.getAddress() + "), is not available"
+                    + " for the display state " + mDeviceState);
+            return null;
+        }
+        LogicalDisplay logicalDisplay = getDisplayLocked(device, /* includeDisabled= */ true);
+        if (logicalDisplay == null) {
+            Slog.w(TAG, "The logical display associated with address (" + display.getAddress()
+                    + "), is not available for the display state " + mDeviceState);
+            return null;
+        }
+        DisplayInfo displayInfo = new DisplayInfo(logicalDisplay.getDisplayInfoLocked());
+        displayInfo.displayId = displayId;
+        return displayInfo;
     }
 
     public void dumpLocked(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index a118b2f..7c647cf 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -599,6 +599,15 @@
                 handleMediaProjectionStoppedLocked(mAppToken);
             }
         }
+
+        @Override
+        public void onCapturedContentResize(int width, int height) {
+            // Do nothing when we tell the client that the content is resized - it is up to them
+            // to decide to update the VirtualDisplay and Surface.
+            // We could only update the VirtualDisplay size, anyway (which the client wouldn't
+            // expect), and there will still be letterboxing on the output content since the
+            // Surface and VirtualDisplay would then have different aspect ratios.
+        }
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java
new file mode 100644
index 0000000..546478e
--- /dev/null
+++ b/services/core/java/com/android/server/display/state/DisplayStateController.java
@@ -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.server.display.state;
+
+import android.hardware.display.DisplayManagerInternal;
+import android.util.IndentingPrintWriter;
+import android.view.Display;
+
+import com.android.server.display.DisplayPowerProximityStateController;
+
+import java.io.PrintWriter;
+
+/**
+ * Maintains the DisplayState of the system.
+ * Internally, this accounts for the proximity changes, and notifying the system
+ * clients about the changes
+ */
+public class DisplayStateController {
+    private DisplayPowerProximityStateController mDisplayPowerProximityStateController;
+    private boolean mPerformScreenOffTransition = false;
+
+    public DisplayStateController(DisplayPowerProximityStateController
+            displayPowerProximityStateController) {
+        this.mDisplayPowerProximityStateController = displayPowerProximityStateController;
+    }
+
+    /**
+     * Updates the DisplayState and notifies the system. Also accounts for the
+     * events being emitted by the proximity sensors
+     *
+     * @param displayPowerRequest   The request to update the display state
+     * @param isDisplayEnabled      A boolean flag representing if the display is enabled
+     * @param isDisplayInTransition A boolean flag representing if the display is undergoing the
+     *                              transition phase
+     */
+    public int updateDisplayState(DisplayManagerInternal.DisplayPowerRequest displayPowerRequest,
+            boolean isDisplayEnabled, boolean isDisplayInTransition) {
+        mPerformScreenOffTransition = false;
+        // Compute the basic display state using the policy.
+        // We might override this below based on other factors.
+        // Initialise brightness as invalid.
+        int state;
+        switch (displayPowerRequest.policy) {
+            case DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF:
+                state = Display.STATE_OFF;
+                mPerformScreenOffTransition = true;
+                break;
+            case DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE:
+                if (displayPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
+                    state = displayPowerRequest.dozeScreenState;
+                } else {
+                    state = Display.STATE_DOZE;
+                }
+                break;
+            case DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM:
+            case DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT:
+            default:
+                state = Display.STATE_ON;
+                break;
+        }
+        assert (state != Display.STATE_UNKNOWN);
+
+        mDisplayPowerProximityStateController.updateProximityState(displayPowerRequest, state);
+
+        if (!isDisplayEnabled || isDisplayInTransition
+                || mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
+            state = Display.STATE_OFF;
+        }
+
+        return state;
+    }
+
+    /**
+     * Checks if the screen off transition is to be performed or not.
+     */
+    public boolean shouldPerformScreenOffTransition() {
+        return mPerformScreenOffTransition;
+    }
+
+    /**
+     * Used to dump the state.
+     *
+     * @param pw The PrintWriter used to dump the state.
+     */
+    public void dumpsys(PrintWriter pw) {
+        pw.println();
+        pw.println("DisplayPowerProximityStateController:");
+        pw.println("  mPerformScreenOffTransition:" + mPerformScreenOffTransition);
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+        if (mDisplayPowerProximityStateController != null) {
+            mDisplayPowerProximityStateController.dumpLocal(ipw);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
index 3c5b067..01cae42 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
@@ -188,7 +188,7 @@
 
         @Override
         public void setUpFsverity(String filePath) throws IOException {
-            VerityUtils.setUpFsverity(filePath, /* signature */ (byte[]) null);
+            VerityUtils.setUpFsverity(filePath);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/incident/IncidentCompanionService.java b/services/core/java/com/android/server/incident/IncidentCompanionService.java
index b8e7d49..87fe785 100644
--- a/services/core/java/com/android/server/incident/IncidentCompanionService.java
+++ b/services/core/java/com/android/server/incident/IncidentCompanionService.java
@@ -34,6 +34,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Log;
 
 import com.android.internal.util.DumpUtils;
@@ -127,21 +128,21 @@
             try {
                 final Context context = getContext();
 
-                // Get the current admin user. Only they can do incident reports.
-                final int currentAdminUser = getCurrentUserIfAdmin();
-                if (currentAdminUser == UserHandle.USER_NULL) {
+                final int primaryUser = getAndValidateUser(context);
+                if (primaryUser == UserHandle.USER_NULL) {
                     return;
                 }
 
                 final Intent intent = new Intent(Intent.ACTION_INCIDENT_REPORT_READY);
                 intent.setComponent(new ComponentName(pkg, cls));
 
-                Log.d(TAG, "sendReportReadyBroadcast sending currentUser=" + currentAdminUser
-                        + " userHandle=" + UserHandle.of(currentAdminUser)
+                Log.d(TAG, "sendReportReadyBroadcast sending primaryUser=" + primaryUser
+                        + " userHandle=" + UserHandle.getUserHandleForUid(primaryUser)
                         + " intent=" + intent);
 
+                // Send it to the primary user.  Only they can do incident reports.
                 context.sendBroadcastAsUserMultiplePermissions(intent,
-                        UserHandle.of(currentAdminUser),
+                        UserHandle.getUserHandleForUid(primaryUser),
                         DUMP_AND_USAGE_STATS_PERMISSIONS);
             } finally {
                 Binder.restoreCallingIdentity(ident);
@@ -413,10 +414,10 @@
     }
 
     /**
-     * Check whether the current user is an admin user, and return the user id if they are.
+     * Check whether the current user is the primary user, and return the user id if they are.
      * Returns UserHandle.USER_NULL if not valid.
      */
-    public static int getCurrentUserIfAdmin() {
+    public static int getAndValidateUser(Context context) {
         // Current user
         UserInfo currentUser;
         try {
@@ -426,21 +427,28 @@
             throw new RuntimeException(ex);
         }
 
+        // Primary user
+        final UserManager um = UserManager.get(context);
+        final UserInfo primaryUser = um.getPrimaryUser();
+
         // Check that we're using the right user.
         if (currentUser == null) {
             Log.w(TAG, "No current user.  Nobody to approve the report."
                     + " The report will be denied.");
             return UserHandle.USER_NULL;
         }
-
-        if (!currentUser.isAdmin()) {
-            Log.w(TAG, "Only an admin user running in foreground can approve "
-                    + "bugreports, but the current foreground user is not an admin user. "
-                    + "The report will be denied.");
+        if (primaryUser == null) {
+            Log.w(TAG, "No primary user.  Nobody to approve the report."
+                    + " The report will be denied.");
+            return UserHandle.USER_NULL;
+        }
+        if (primaryUser.id != currentUser.id) {
+            Log.w(TAG, "Only the primary user can approve bugreports, but they are not"
+                    + " the current user. The report will be denied.");
             return UserHandle.USER_NULL;
         }
 
-        return currentUser.id;
+        return primaryUser.id;
     }
 }
 
diff --git a/services/core/java/com/android/server/incident/PendingReports.java b/services/core/java/com/android/server/incident/PendingReports.java
index 6285bc3..f39bebf 100644
--- a/services/core/java/com/android/server/incident/PendingReports.java
+++ b/services/core/java/com/android/server/incident/PendingReports.java
@@ -16,7 +16,6 @@
 
 package com.android.server.incident;
 
-import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.content.ComponentName;
@@ -273,19 +272,15 @@
             return;
         }
 
-        // Find the current user of the device and check if they are an admin.
-        final int currentAdminUser = getCurrentUserIfAdmin();
-
-        // Deny the report if the current admin user is null
-        // or not the user who requested the report.
-        if (currentAdminUser == UserHandle.USER_NULL
-                || currentAdminUser != UserHandle.getUserId(callingUid)) {
+        // Find the primary user of this device.
+        final int primaryUser = getAndValidateUser();
+        if (primaryUser == UserHandle.USER_NULL) {
             denyReportBeforeAddingRec(listener, callingPackage);
             return;
         }
 
         // Find the approver app (hint: it's PermissionController).
-        final ComponentName receiver = getApproverComponent(currentAdminUser);
+        final ComponentName receiver = getApproverComponent(primaryUser);
         if (receiver == null) {
             // We couldn't find an approver... so deny the request here and now, before we
             // do anything else.
@@ -303,26 +298,26 @@
         try {
             listener.asBinder().linkToDeath(() -> {
                 Log.i(TAG, "Got death notification listener=" + listener);
-                cancelReportImpl(listener, receiver, currentAdminUser);
+                cancelReportImpl(listener, receiver, primaryUser);
             }, 0);
         } catch (RemoteException ex) {
             Log.e(TAG, "Remote died while trying to register death listener: " + rec.getUri());
             // First, remove from our list.
-            cancelReportImpl(listener, receiver, currentAdminUser);
+            cancelReportImpl(listener, receiver, primaryUser);
         }
 
         // Go tell Permission controller to start asking the user.
-        sendBroadcast(receiver, currentAdminUser);
+        sendBroadcast(receiver, primaryUser);
     }
 
     /**
      * Cancel a pending report request (because of an explicit call to cancel)
      */
     private void cancelReportImpl(IIncidentAuthListener listener) {
-        final int currentAdminUser = getCurrentUserIfAdmin();
-        final ComponentName receiver = getApproverComponent(currentAdminUser);
-        if (currentAdminUser != UserHandle.USER_NULL && receiver != null) {
-            cancelReportImpl(listener, receiver, currentAdminUser);
+        final int primaryUser = getAndValidateUser();
+        final ComponentName receiver = getApproverComponent(primaryUser);
+        if (primaryUser != UserHandle.USER_NULL && receiver != null) {
+            cancelReportImpl(listener, receiver, primaryUser);
         }
     }
 
@@ -331,13 +326,13 @@
      * by the calling app, or because of a binder death).
      */
     private void cancelReportImpl(IIncidentAuthListener listener, ComponentName receiver,
-            @UserIdInt int user) {
+            int primaryUser) {
         // First, remove from our list.
         synchronized (mLock) {
             removePendingReportRecLocked(listener);
         }
         // Second, call back to PermissionController to say it's canceled.
-        sendBroadcast(receiver, user);
+        sendBroadcast(receiver, primaryUser);
     }
 
     /**
@@ -347,21 +342,21 @@
      * cleanup cases to keep the apps' list in sync with ours.
      */
     private void sendBroadcast() {
-        final int currentAdminUser = getCurrentUserIfAdmin();
-        if (currentAdminUser == UserHandle.USER_NULL) {
+        final int primaryUser = getAndValidateUser();
+        if (primaryUser == UserHandle.USER_NULL) {
             return;
         }
-        final ComponentName receiver = getApproverComponent(currentAdminUser);
+        final ComponentName receiver = getApproverComponent(primaryUser);
         if (receiver == null) {
             return;
         }
-        sendBroadcast(receiver, currentAdminUser);
+        sendBroadcast(receiver, primaryUser);
     }
 
     /**
      * Send the confirmation broadcast.
      */
-    private void sendBroadcast(ComponentName receiver, int currentUser) {
+    private void sendBroadcast(ComponentName receiver, int primaryUser) {
         final Intent intent = new Intent(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
         intent.setComponent(receiver);
         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
@@ -369,8 +364,8 @@
         final BroadcastOptions options = BroadcastOptions.makeBasic();
         options.setBackgroundActivityStartsAllowed(true);
 
-        // Send it to the current user.
-        mContext.sendBroadcastAsUser(intent, UserHandle.of(currentUser),
+        // Send it to the primary user.
+        mContext.sendBroadcastAsUser(intent, UserHandle.getUserHandleForUid(primaryUser),
                 android.Manifest.permission.APPROVE_INCIDENT_REPORTS, options.toBundle());
     }
 
@@ -425,11 +420,11 @@
     }
 
     /**
-     * Check whether the current user is an admin user, and return the user id if they are.
+     * Check whether the current user is the primary user, and return the user id if they are.
      * Returns UserHandle.USER_NULL if not valid.
      */
-    private int getCurrentUserIfAdmin() {
-        return IncidentCompanionService.getCurrentUserIfAdmin();
+    private int getAndValidateUser() {
+        return IncidentCompanionService.getAndValidateUser(mContext);
     }
 
     /**
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 298098a..01a564d 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -171,4 +171,19 @@
      * {@see Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT}
      */
     public abstract void decrementKeyboardBacklight(int deviceId);
+
+    /**
+     * Add a runtime association between the input port and device type. Input ports are expected to
+     * be unique.
+     * @param inputPort The port of the input device.
+     * @param type The type of the device. E.g. "touchNavigation".
+     */
+    public abstract void setTypeAssociation(@NonNull String inputPort, @NonNull String type);
+
+    /**
+     * Removes a runtime association between the input device and type.
+     *
+     * @param inputPort The port of the input device.
+     */
+    public abstract void unsetTypeAssociation(@NonNull String inputPort);
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index c62abf0..1809b18 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -248,6 +248,13 @@
     @GuardedBy("mAssociationsLock")
     private final Map<String, String> mUniqueIdAssociations = new ArrayMap<>();
 
+    // Stores input ports associated with device types. For example, adding an association
+    // {"123", "touchNavigation"} here would mean that a touch device appearing at port "123" would
+    // enumerate as a "touch navigation" device rather than the default "touchpad as a mouse
+    // pointer" device.
+    @GuardedBy("mAssociationsLock")
+    private final Map<String, String> mDeviceTypeAssociations = new ArrayMap<>();
+
     // Guards per-display input properties and properties relating to the mouse pointer.
     // Threads can wait on this lock to be notified the next time the display on which the mouse
     // pointer is shown has changed.
@@ -1905,6 +1912,23 @@
         mNative.changeUniqueIdAssociation();
     }
 
+    void setTypeAssociationInternal(@NonNull String inputPort, @NonNull String type) {
+        Objects.requireNonNull(inputPort);
+        Objects.requireNonNull(type);
+        synchronized (mAssociationsLock) {
+            mDeviceTypeAssociations.put(inputPort, type);
+        }
+        mNative.changeTypeAssociation();
+    }
+
+    void unsetTypeAssociationInternal(@NonNull String inputPort) {
+        Objects.requireNonNull(inputPort);
+        synchronized (mAssociationsLock) {
+            mDeviceTypeAssociations.remove(inputPort);
+        }
+        mNative.changeTypeAssociation();
+    }
+
     @Override // Binder call
     public InputSensorInfo[] getSensorList(int deviceId) {
         return mNative.getSensorList(deviceId);
@@ -2221,6 +2245,13 @@
                     pw.println("  uniqueId: " + v);
                 });
             }
+            if (!mDeviceTypeAssociations.isEmpty()) {
+                pw.println("Type Associations:");
+                mDeviceTypeAssociations.forEach((k, v) -> {
+                    pw.print("  port: " + k);
+                    pw.println("  type: " + v);
+                });
+            }
         }
     }
 
@@ -2630,6 +2661,18 @@
         return flatten(associations);
     }
 
+    // Native callback
+    @SuppressWarnings("unused")
+    @VisibleForTesting
+    String[] getDeviceTypeAssociations() {
+        final Map<String, String> associations;
+        synchronized (mAssociationsLock) {
+            associations = new HashMap<>(mDeviceTypeAssociations);
+        }
+
+        return flatten(associations);
+    }
+
     /**
      * Gets if an input device could dispatch to the given display".
      * @param deviceId The input device id.
@@ -3263,6 +3306,16 @@
         public void decrementKeyboardBacklight(int deviceId) {
             mKeyboardBacklightController.decrementKeyboardBacklight(deviceId);
         }
+
+        @Override
+        public void setTypeAssociation(@NonNull String inputPort, @NonNull String type) {
+            setTypeAssociationInternal(inputPort, type);
+        }
+
+        @Override
+        public void unsetTypeAssociation(@NonNull String inputPort) {
+            unsetTypeAssociationInternal(inputPort);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 8781c6e..184bc0e 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -186,6 +186,8 @@
 
     void changeUniqueIdAssociation();
 
+    void changeTypeAssociation();
+
     void notifyPointerDisplayIdChanged();
 
     void setDisplayEligibilityForPointerCapture(int displayId, boolean enabled);
@@ -400,6 +402,9 @@
         public native void changeUniqueIdAssociation();
 
         @Override
+        public native void changeTypeAssociation();
+
+        @Override
         public native void notifyPointerDisplayIdChanged();
 
         @Override
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 2dc1e1c..5f39a52 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -240,8 +240,7 @@
     protected final UserManager mUserManager;
     private final IStorageManager mStorageManager;
     private final IActivityManager mActivityManager;
-    @VisibleForTesting
-    protected final SyntheticPasswordManager mSpManager;
+    private final SyntheticPasswordManager mSpManager;
 
     private final java.security.KeyStore mJavaKeyStore;
     private final RecoverableKeyStoreManager mRecoverableKeyStoreManager;
@@ -946,7 +945,7 @@
                     if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
                         Slogf.i(TAG, "Creating locksettings state for user %d now that boot "
                                 + "is complete", userId);
-                        initializeSyntheticPasswordLocked(userId);
+                        initializeSyntheticPassword(userId);
                     }
                 }
             }
@@ -985,7 +984,7 @@
         long protectorId = getCurrentLskfBasedProtectorId(userId);
         if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
             Slogf.i(TAG, "Migrating unsecured user %d to SP-based credential", userId);
-            initializeSyntheticPasswordLocked(userId);
+            initializeSyntheticPassword(userId);
         } else {
             Slogf.i(TAG, "Existing unsecured user %d has a synthetic password; re-encrypting CE " +
                     "key with it", userId);
@@ -1027,11 +1026,18 @@
     }
 
     private void enforceFrpResolved() {
+        final int mainUserId = mInjector.getUserManagerInternal().getMainUserId();
+        if (mainUserId < 0) {
+            Slog.i(TAG, "No Main user on device; skip enforceFrpResolved");
+            return;
+        }
         final ContentResolver cr = mContext.getContentResolver();
+
         final boolean inSetupWizard = Settings.Secure.getIntForUser(cr,
-                Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_SYSTEM) == 0;
-        final boolean secureFrp = Settings.Secure.getIntForUser(cr,
-                Settings.Secure.SECURE_FRP_MODE, 0, UserHandle.USER_SYSTEM) == 1;
+                Settings.Secure.USER_SETUP_COMPLETE, 0, mainUserId) == 0;
+        final boolean secureFrp = Settings.Global.getInt(cr,
+                Settings.Global.SECURE_FRP_MODE, 0) == 1;
+
         if (inSetupWizard && secureFrp) {
             throw new SecurityException("Cannot change credential in SUW while factory reset"
                     + " protection is not resolved yet");
@@ -1645,13 +1651,7 @@
         Objects.requireNonNull(savedCredential);
         if (DEBUG) Slog.d(TAG, "setLockCredentialInternal: user=" + userId);
         synchronized (mSpManager) {
-            if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
-                if (!savedCredential.isNone()) {
-                    throw new IllegalStateException("Saved credential given, but user has no SP");
-                }
-                // TODO(b/232452368): this case is only needed by unit tests now; remove it.
-                initializeSyntheticPasswordLocked(userId);
-            } else if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) {
+            if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) {
                 // get credential from keystore when profile has unified lock
                 try {
                     //TODO: remove as part of b/80170828
@@ -2315,9 +2315,7 @@
                 return;
             }
             removeStateForReusedUserIdIfNecessary(userId, userSerialNumber);
-            synchronized (mSpManager) {
-                initializeSyntheticPasswordLocked(userId);
-            }
+            initializeSyntheticPassword(userId);
         }
     }
 
@@ -2643,21 +2641,22 @@
      * until the time when Weaver is guaranteed to be available), or when upgrading from Android 13
      * or earlier where users with no LSKF didn't necessarily have an SP.
      */
-    @GuardedBy("mSpManager")
     @VisibleForTesting
-    SyntheticPassword initializeSyntheticPasswordLocked(int userId) {
-        Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
-        Preconditions.checkState(getCurrentLskfBasedProtectorId(userId) ==
-                SyntheticPasswordManager.NULL_PROTECTOR_ID,
-                "Cannot reinitialize SP");
+    SyntheticPassword initializeSyntheticPassword(int userId) {
+        synchronized (mSpManager) {
+            Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
+            Preconditions.checkState(getCurrentLskfBasedProtectorId(userId) ==
+                    SyntheticPasswordManager.NULL_PROTECTOR_ID,
+                    "Cannot reinitialize SP");
 
-        final SyntheticPassword sp = mSpManager.newSyntheticPassword(userId);
-        final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
-                LockscreenCredential.createNone(), sp, userId);
-        setCurrentLskfBasedProtectorId(protectorId, userId);
-        setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
-        onSyntheticPasswordKnown(userId, sp);
-        return sp;
+            final SyntheticPassword sp = mSpManager.newSyntheticPassword(userId);
+            final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
+                    LockscreenCredential.createNone(), sp, userId);
+            setCurrentLskfBasedProtectorId(protectorId, userId);
+            setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
+            onSyntheticPasswordKnown(userId, sp);
+            return sp;
+        }
     }
 
     @VisibleForTesting
@@ -2673,13 +2672,6 @@
         setLong(LSKF_LAST_CHANGED_TIME_KEY, System.currentTimeMillis(), userId);
     }
 
-    @VisibleForTesting
-    boolean isSyntheticPasswordBasedCredential(int userId) {
-        synchronized (mSpManager) {
-            return isSyntheticPasswordBasedCredentialLocked(userId);
-        }
-    }
-
     private boolean isSyntheticPasswordBasedCredentialLocked(int userId) {
         if (userId == USER_FRP) {
             final int type = mStorage.readPersistentDataBlock().type;
@@ -2918,19 +2910,14 @@
             @NonNull EscrowTokenStateChangeCallback callback) {
         if (DEBUG) Slog.d(TAG, "addEscrowToken: user=" + userId + ", type=" + type);
         synchronized (mSpManager) {
-            // If the user has no LSKF, then the token can be activated immediately, after creating
-            // the user's SP if it doesn't already exist.  Otherwise, the token can't be activated
-            // until the SP is unlocked by another protector (normally the LSKF-based one).
+            // If the user has no LSKF, then the token can be activated immediately.  Otherwise, the
+            // token can't be activated until the SP is unlocked by another protector (normally the
+            // LSKF-based one).
             SyntheticPassword sp = null;
             if (!isUserSecure(userId)) {
                 long protectorId = getCurrentLskfBasedProtectorId(userId);
-                if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
-                    // TODO(b/232452368): this case is only needed by unit tests now; remove it.
-                    sp = initializeSyntheticPasswordLocked(userId);
-                } else {
-                    sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
-                            LockscreenCredential.createNone(), userId, null).syntheticPassword;
-                }
+                sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
+                        LockscreenCredential.createNone(), userId, null).syntheticPassword;
             }
             disableEscrowTokenOnNonManagedDevicesIfNeeded(userId);
             if (!mSpManager.hasEscrowData(userId)) {
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index ed8d852..50e1fca 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -315,7 +315,7 @@
         @Override // Binder call
         public boolean isValidMediaProjection(IMediaProjection projection) {
             return MediaProjectionManagerService.this.isValidMediaProjection(
-                    projection.asBinder());
+                    projection == null ? null : projection.asBinder());
         }
 
         @Override // Binder call
@@ -348,7 +348,26 @@
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
+        }
 
+        @Override // Binder call
+        public void notifyActiveProjectionCapturedContentResized(int width, int height) {
+            if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify "
+                        + "on captured content resize");
+            }
+            if (!isValidMediaProjection(mProjectionGrant)) {
+                return;
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                if (mProjectionGrant != null && mCallbackDelegate != null) {
+                    mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
         }
 
         @Override //Binder call
@@ -659,9 +678,11 @@
 
     private static class CallbackDelegate {
         private Map<IBinder, IMediaProjectionCallback> mClientCallbacks;
+        // Map from the IBinder token representing the callback, to the callback instance.
+        // Represents the callbacks registered on the client's MediaProjectionManager.
         private Map<IBinder, IMediaProjectionWatcherCallback> mWatcherCallbacks;
         private Handler mHandler;
-        private Object mLock = new Object();
+        private final Object mLock = new Object();
 
         public CallbackDelegate() {
             mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
@@ -715,6 +736,8 @@
             }
             synchronized (mLock) {
                 for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
+                    // Notify every callback the client has registered for a particular
+                    // MediaProjection instance.
                     mHandler.post(new ClientStopCallback(callback));
                 }
 
@@ -724,6 +747,33 @@
                 }
             }
         }
+
+        public void dispatchResize(MediaProjection projection, int width, int height) {
+            if (projection == null) {
+                Slog.e(TAG, "Tried to dispatch stop notification for a null media projection."
+                        + " Ignoring!");
+                return;
+            }
+            synchronized (mLock) {
+                // TODO(b/249827847) Currently the service assumes there is only one projection
+                //  at once - need to find the callback for the given projection, when there are
+                //  multiple sessions.
+                for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
+                    mHandler.post(() -> {
+                        try {
+                            // Notify every callback the client has registered for a particular
+                            // MediaProjection instance.
+                            callback.onCapturedContentResize(width, height);
+                        } catch (RemoteException e) {
+                            Slog.w(TAG, "Failed to notify media projection has resized to " + width
+                                    + " x " + height, e);
+                        }
+                    });
+                }
+                // Do not need to notify watcher callback about resize, since watcher callback
+                // is for passing along if recording is still ongoing or not.
+            }
+        }
     }
 
     private static final class WatcherStartCallback implements Runnable {
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 2fdc4cd..58428ca 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -33,8 +33,8 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.telephony.TelephonyManager;
-import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Slog;
 
@@ -189,10 +189,10 @@
     }
 
     /**
-     * Validates that the current user is an admin user or, when bugreport is requested remotely
-     * that the current user is an affiliated user.
+     * Validates that the current user is the primary user or when bugreport is requested remotely
+     * and current user is affiliated user.
      *
-     * @throws IllegalArgumentException if the current user is not an admin user
+     * @throws IllegalArgumentException if the current user is not the primary user
      */
     private void ensureUserCanTakeBugReport(int bugreportMode) {
         UserInfo currentUser = null;
@@ -202,17 +202,20 @@
             // Impossible to get RemoteException for an in-process call.
         }
 
+        UserInfo primaryUser = UserManager.get(mContext).getPrimaryUser();
         if (currentUser == null) {
-            logAndThrow("There is no current user, so no bugreport can be requested.");
+            logAndThrow("No current user. Only primary user is allowed to take bugreports.");
         }
-
-        if (!currentUser.isAdmin()) {
+        if (primaryUser == null) {
+            logAndThrow("No primary user. Only primary user is allowed to take bugreports.");
+        }
+        if (primaryUser.id != currentUser.id) {
             if (bugreportMode == BugreportParams.BUGREPORT_MODE_REMOTE
                     && isCurrentUserAffiliated(currentUser.id)) {
                 return;
             }
-            logAndThrow(TextUtils.formatSimple("Current user %s is not an admin user."
-                    + " Only admin users are allowed to take bugreport.", currentUser.id));
+            logAndThrow("Current user not primary user. Only primary user"
+                    + " is allowed to take bugreports.");
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index df95f86..d4c4c69 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -17,19 +17,46 @@
 package com.android.server.pm;
 
 import android.annotation.NonNull;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.IBackgroundInstallControlService;
 import android.content.pm.IPackageManager;
+import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
-import android.os.IBinder;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Slog;
 import android.util.SparseArrayMap;
+import android.util.SparseSetArray;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
 import com.android.server.SystemService;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.TreeSet;
 
 /**
  * @hide
@@ -37,14 +64,30 @@
 public class BackgroundInstallControlService extends SystemService {
     private static final String TAG = "BackgroundInstallControlService";
 
+    private static final String DISK_FILE_NAME = "states";
+    private static final String DISK_DIR_NAME = "bic";
+
+    private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10;
+
+    private static final int MSG_USAGE_EVENT_RECEIVED = 0;
+    private static final int MSG_PACKAGE_ADDED = 1;
+    private static final int MSG_PACKAGE_REMOVED = 2;
+
     private final Context mContext;
     private final BinderService mBinderService;
     private final IPackageManager mIPackageManager;
+    private final PackageManagerInternal mPackageManagerInternal;
+    private final UsageStatsManagerInternal mUsageStatsManagerInternal;
+    private final PermissionManagerServiceInternal mPermissionManager;
+    private final Handler mHandler;
+    private final File mDiskFile;
 
-    // User ID -> package name -> time diff
-    // The time diff between the last foreground activity installer and
-    // the "onPackageAdded" function call.
-    private final SparseArrayMap<String, Long> mBackgroundInstalledPackages =
+
+    private SparseSetArray<String> mBackgroundInstalledPackages = null;
+
+    // User ID -> package name -> set of foreground time frame
+    private final SparseArrayMap<String,
+            TreeSet<ForegroundTimeFrame>> mInstallerForegroundTimeFrames =
             new SparseArrayMap<>();
 
     public BackgroundInstallControlService(@NonNull Context context) {
@@ -56,49 +99,385 @@
         super(injector.getContext());
         mContext = injector.getContext();
         mIPackageManager = injector.getIPackageManager();
+        mPackageManagerInternal = injector.getPackageManagerInternal();
+        mPermissionManager = injector.getPermissionManager();
+        mHandler = new EventHandler(injector.getLooper(), this);
+        mDiskFile = injector.getDiskFile();
+        mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal();
+        mUsageStatsManagerInternal.registerListener(
+                (userId, event) ->
+                        mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED,
+                                userId,
+                                0,
+                                event).sendToTarget()
+        );
         mBinderService = new BinderService(this);
     }
 
     private static final class BinderService extends IBackgroundInstallControlService.Stub {
         final BackgroundInstallControlService mService;
 
-        BinderService(BackgroundInstallControlService service)  {
+        BinderService(BackgroundInstallControlService service) {
             mService = service;
         }
 
         @Override
         public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
                 @PackageManager.PackageInfoFlagsBits long flags, int userId) {
-            ParceledListSlice<PackageInfo> packages;
-            try {
-                packages = mService.mIPackageManager.getInstalledPackages(flags, userId);
-            } catch (RemoteException e) {
-                throw new IllegalStateException("Package manager not available", e);
-            }
-
-            // TODO(b/244216300): to enable the test the usage by BinaryTransparencyService,
-            // we currently comment out the actual implementation.
-            // The fake implementation is just to filter out the first app of the list.
-            // for (int i = 0, size = packages.getList().size(); i < size; i++) {
-            //     String packageName = packages.getList().get(i).packageName;
-            //     if (!mBackgroundInstalledPackages.contains(userId, packageName) {
-            //         packages.getList().remove(i);
-            //     }
-            // }
-            if (packages.getList().size() > 0) {
-                packages.getList().remove(0);
-            }
-            return packages;
+            return mService.getBackgroundInstalledPackages(flags, userId);
         }
     }
 
-    /**
-     * Called when the system service should publish a binder service using
-     * {@link #publishBinderService(String, IBinder).}
-     */
+    @VisibleForTesting
+    ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
+            @PackageManager.PackageInfoFlagsBits long flags, int userId) {
+        ParceledListSlice<PackageInfo> packages;
+        try {
+            packages = mIPackageManager.getInstalledPackages(flags, userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        initBackgroundInstalledPackages();
+
+        ListIterator<PackageInfo> iter = packages.getList().listIterator();
+        while (iter.hasNext()) {
+            String packageName = iter.next().packageName;
+            if (!mBackgroundInstalledPackages.contains(userId, packageName)) {
+                iter.remove();
+            }
+        }
+
+        return packages;
+    }
+
+    private static class EventHandler extends Handler {
+        private final BackgroundInstallControlService mService;
+
+        EventHandler(Looper looper, BackgroundInstallControlService service) {
+            super(looper);
+            mService = service;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_USAGE_EVENT_RECEIVED: {
+                    mService.handleUsageEvent((UsageEvents.Event) msg.obj, msg.arg1 /* userId */);
+                    break;
+                }
+                case MSG_PACKAGE_ADDED: {
+                    mService.handlePackageAdd((String) msg.obj, msg.arg1 /* userId */);
+                    break;
+                }
+                case MSG_PACKAGE_REMOVED: {
+                    mService.handlePackageRemove((String) msg.obj, msg.arg1 /* userId */);
+                    break;
+                }
+                default:
+                    Slog.w(TAG, "Unknown message: " + msg.what);
+            }
+        }
+    }
+
+    void handlePackageAdd(String packageName, int userId) {
+        InstallSourceInfo installSourceInfo = null;
+        try {
+            installSourceInfo = mIPackageManager.getInstallSourceInfo(packageName);
+        } catch (RemoteException e) {
+            // Failed to talk to PackageManagerService Should never happen!
+            throw e.rethrowFromSystemServer();
+        }
+        String installerPackageName =
+                installSourceInfo == null ? null : installSourceInfo.getInstallingPackageName();
+        if (installerPackageName == null) {
+            Slog.w(TAG, "fails to get installerPackageName for " + packageName);
+            return;
+        }
+
+        ApplicationInfo appInfo = null;
+        try {
+            appInfo = mIPackageManager.getApplicationInfo(packageName,
+                    0, userId);
+        } catch (RemoteException e) {
+            // Failed to talk to PackageManagerService Should never happen!
+            throw e.rethrowFromSystemServer();
+        }
+
+        if (appInfo == null) {
+            Slog.w(TAG, "fails to get appInfo for " + packageName);
+            return;
+        }
+
+        // convert up-time to current time.
+        final long installTimestamp = System.currentTimeMillis()
+                - (SystemClock.uptimeMillis() - appInfo.createTimestamp);
+
+        if (wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
+            return;
+        }
+
+        initBackgroundInstalledPackages();
+        mBackgroundInstalledPackages.add(userId, packageName);
+        writeBackgroundInstalledPackagesToDisk();
+    }
+
+    private boolean wasForegroundInstallation(String installerPackageName,
+            int userId, long installTimestamp) {
+        TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
+                mInstallerForegroundTimeFrames.get(userId, installerPackageName);
+
+        // The installer never run in foreground.
+        if (foregroundTimeFrames == null) {
+            return false;
+        }
+
+        for (var foregroundTimeFrame : foregroundTimeFrames) {
+            // the foreground time frame starts later than the installation.
+            // so the installation is outside the foreground time frame.
+            if (foregroundTimeFrame.startTimeStampMillis > installTimestamp) {
+                continue;
+            }
+
+            // the foreground time frame is not over yet.
+            // the installation is inside the foreground time frame.
+            if (!foregroundTimeFrame.isDone()) {
+                return true;
+            }
+
+            // the foreground time frame ends later than the installation.
+            // the installation is inside the foreground time frame.
+            if (installTimestamp <= foregroundTimeFrame.endTimeStampMillis) {
+                return true;
+            }
+        }
+
+        // the installation is not inside any of foreground time frames.
+        // so it is not a foreground installation.
+        return false;
+    }
+
+    void handlePackageRemove(String packageName, int userId) {
+        initBackgroundInstalledPackages();
+        mBackgroundInstalledPackages.remove(userId, packageName);
+        writeBackgroundInstalledPackagesToDisk();
+    }
+
+    void handleUsageEvent(UsageEvents.Event event, int userId) {
+        if (event.mEventType != UsageEvents.Event.ACTIVITY_RESUMED
+                && event.mEventType != UsageEvents.Event.ACTIVITY_PAUSED
+                && event.mEventType != UsageEvents.Event.ACTIVITY_STOPPED) {
+            return;
+        }
+
+        if (!isInstaller(event.mPackage, userId)) {
+            return;
+        }
+
+        if (!mInstallerForegroundTimeFrames.contains(userId, event.mPackage)) {
+            mInstallerForegroundTimeFrames.add(userId, event.mPackage, new TreeSet<>());
+        }
+
+        TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
+                mInstallerForegroundTimeFrames.get(userId, event.mPackage);
+
+        if ((foregroundTimeFrames.size() == 0) || foregroundTimeFrames.last().isDone()) {
+            // ignore the other events if there is no open ForegroundTimeFrame.
+            if (event.mEventType != UsageEvents.Event.ACTIVITY_RESUMED) {
+                return;
+            }
+            foregroundTimeFrames.add(new ForegroundTimeFrame(event.mTimeStamp));
+        }
+
+        foregroundTimeFrames.last().addEvent(event);
+
+        if (foregroundTimeFrames.size() > MAX_FOREGROUND_TIME_FRAMES_SIZE) {
+            foregroundTimeFrames.pollFirst();
+        }
+    }
+
+    @VisibleForTesting
+    void writeBackgroundInstalledPackagesToDisk() {
+        AtomicFile atomicFile = new AtomicFile(mDiskFile);
+        FileOutputStream fileOutputStream;
+        try {
+            fileOutputStream = atomicFile.startWrite();
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to start write to states protobuf.", e);
+            return;
+        }
+
+        try {
+            ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
+            for (int i = 0; i < mBackgroundInstalledPackages.size(); i++) {
+                int userId = mBackgroundInstalledPackages.keyAt(i);
+                for (String packageName : mBackgroundInstalledPackages.get(userId)) {
+                    long token = protoOutputStream.start(
+                            BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                    protoOutputStream.write(
+                            BackgroundInstalledPackageProto.PACKAGE_NAME, packageName);
+                    protoOutputStream.write(
+                            BackgroundInstalledPackageProto.USER_ID, userId + 1);
+                    protoOutputStream.end(token);
+                }
+            }
+            protoOutputStream.flush();
+            atomicFile.finishWrite(fileOutputStream);
+        } catch (Exception e) {
+            Slog.e(TAG, "Failed to finish write to states protobuf.", e);
+            atomicFile.failWrite(fileOutputStream);
+        }
+    }
+
+    @VisibleForTesting
+    void initBackgroundInstalledPackages() {
+        if (mBackgroundInstalledPackages != null) {
+            return;
+        }
+
+        mBackgroundInstalledPackages = new SparseSetArray<>();
+
+        if (!mDiskFile.exists()) {
+            return;
+        }
+
+        AtomicFile atomicFile = new AtomicFile(mDiskFile);
+        try (FileInputStream fileInputStream = atomicFile.openRead()) {
+            ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
+
+            while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (protoInputStream.getFieldNumber()
+                        != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
+                    continue;
+                }
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                String packageName = null;
+                int userId = UserHandle.USER_NULL;
+                while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (protoInputStream.getFieldNumber()) {
+                        case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            break;
+                        case (int) BackgroundInstalledPackageProto.USER_ID:
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            break;
+                        default:
+                            Slog.w(TAG, "Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
+                    }
+                }
+                protoInputStream.end(token);
+                if (packageName != null && userId != UserHandle.USER_NULL) {
+                    mBackgroundInstalledPackages.add(userId, packageName);
+                } else {
+                    Slog.w(TAG, "Fails to get packageName or UserId from proto file");
+                }
+            }
+        } catch (IOException e) {
+            Slog.w(TAG, "Error reading state from the disk", e);
+        }
+    }
+
+    @VisibleForTesting
+    SparseSetArray<String> getBackgroundInstalledPackages() {
+        return mBackgroundInstalledPackages;
+    }
+
+    @VisibleForTesting
+    SparseArrayMap<String, TreeSet<ForegroundTimeFrame>> getInstallerForegroundTimeFrames() {
+        return mInstallerForegroundTimeFrames;
+    }
+
+    private boolean isInstaller(String pkgName, int userId) {
+        if (mInstallerForegroundTimeFrames.contains(userId, pkgName)) {
+            return true;
+        }
+        return mPermissionManager.checkPermission(pkgName,
+                android.Manifest.permission.INSTALL_PACKAGES,
+                userId) == PackageManager.PERMISSION_GRANTED;
+    }
+
     @Override
     public void onStart() {
-        publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
+        onStart(/* isForTesting= */ false);
+    }
+
+    @VisibleForTesting
+    void onStart(boolean isForTesting) {
+        if (!isForTesting) {
+            publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
+        }
+
+        mPackageManagerInternal.getPackageList(new PackageManagerInternal.PackageListObserver() {
+            @Override
+            public void onPackageAdded(String packageName, int uid) {
+                final int userId = UserHandle.getUserId(uid);
+                mHandler.obtainMessage(MSG_PACKAGE_ADDED,
+                        userId, 0, packageName).sendToTarget();
+            }
+
+            @Override
+            public void onPackageRemoved(String packageName, int uid) {
+                final int userId = UserHandle.getUserId(uid);
+                mHandler.obtainMessage(MSG_PACKAGE_REMOVED,
+                        userId, 0, packageName).sendToTarget();
+            }
+        });
+    }
+
+    // The foreground time frame (ForegroundTimeFrame) represents the period
+    // when a package's activities continuously occupy the foreground.
+    // Each ForegroundTimeFrame starts with an ACTIVITY_RESUMED event,
+    // and then ends with an ACTIVITY_PAUSED or ACTIVITY_STOPPED event.
+    // The startTimeStampMillis stores the timestamp of the ACTIVITY_RESUMED event.
+    // The endTimeStampMillis stores the timestamp of the ACTIVITY_PAUSED or ACTIVITY_STOPPED event
+    // that wraps up the ForegroundTimeFrame.
+    // The activities are designed to handle the edge case in which a package's one activity
+    // seamlessly replace another activity of the same package. Thus, we count these activities
+    // together as a ForegroundTimeFrame. For this scenario, only when all the activities terminate
+    // shall consider the completion of the ForegroundTimeFrame.
+    static final class ForegroundTimeFrame implements Comparable<ForegroundTimeFrame> {
+        public final long startTimeStampMillis;
+        public long endTimeStampMillis;
+        public final Set<Integer> activities;
+
+        public int compareTo(ForegroundTimeFrame o) {
+            int comp = Long.compare(startTimeStampMillis, o.startTimeStampMillis);
+            if (comp != 0) return comp;
+
+            return Integer.compare(hashCode(), o.hashCode());
+        }
+
+        ForegroundTimeFrame(long startTimeStampMillis) {
+            this.startTimeStampMillis = startTimeStampMillis;
+            endTimeStampMillis = 0;
+            activities = new ArraySet<>();
+        }
+
+        public boolean isDone() {
+            return endTimeStampMillis != 0;
+        }
+
+        public void addEvent(UsageEvents.Event event) {
+            switch (event.mEventType) {
+                case UsageEvents.Event.ACTIVITY_RESUMED:
+                    activities.add(event.mInstanceId);
+                    break;
+                case UsageEvents.Event.ACTIVITY_PAUSED:
+                case UsageEvents.Event.ACTIVITY_STOPPED:
+                    if (activities.contains(event.mInstanceId)) {
+                        activities.remove(event.mInstanceId);
+                        if (activities.size() == 0) {
+                            endTimeStampMillis = event.mTimeStamp;
+                        }
+                    }
+                    break;
+                default:
+            }
+        }
     }
 
     /**
@@ -108,6 +487,16 @@
         Context getContext();
 
         IPackageManager getIPackageManager();
+
+        PackageManagerInternal getPackageManagerInternal();
+
+        UsageStatsManagerInternal getUsageStatsManagerInternal();
+
+        PermissionManagerServiceInternal getPermissionManager();
+
+        Looper getLooper();
+
+        File getDiskFile();
     }
 
     private static final class InjectorImpl implements Injector {
@@ -126,5 +515,36 @@
         public IPackageManager getIPackageManager() {
             return IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
         }
+
+        @Override
+        public PackageManagerInternal getPackageManagerInternal() {
+            return LocalServices.getService(PackageManagerInternal.class);
+        }
+
+        @Override
+        public UsageStatsManagerInternal getUsageStatsManagerInternal() {
+            return LocalServices.getService(UsageStatsManagerInternal.class);
+        }
+
+        @Override
+        public PermissionManagerServiceInternal getPermissionManager() {
+            return LocalServices.getService(PermissionManagerServiceInternal.class);
+        }
+
+        @Override
+        public Looper getLooper() {
+            ServiceThread serviceThread = new ServiceThread(TAG,
+                    android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
+            serviceThread.start();
+            return serviceThread.getLooper();
+
+        }
+
+        @Override
+        public File getDiskFile() {
+            File dir = new File(Environment.getDataSystemDirectory(), DISK_DIR_NAME);
+            File file = new File(dir, DISK_FILE_NAME);
+            return file;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index 60621a0..ea84283 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -570,8 +570,9 @@
     @PackageManager.InstallReason
     int getInstallReason(@NonNull String packageName, @UserIdInt int userId);
 
-    boolean canPackageQuery(@NonNull String sourcePackageName, @NonNull String targetPackageName,
-            @UserIdInt int userId);
+    @NonNull
+    boolean[] canPackageQuery(@NonNull String sourcePackageName,
+            @NonNull String[] targetPackageNames, @UserIdInt int userId);
 
     boolean canForwardTo(@NonNull Intent intent, @Nullable String resolvedType,
             @UserIdInt int sourceUserId, @UserIdInt int targetUserId);
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index b8fba51..06aadd92 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -159,6 +159,7 @@
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -5337,29 +5338,42 @@
     }
 
     @Override
-    public boolean canPackageQuery(@NonNull String sourcePackageName,
-            @NonNull String targetPackageName, @UserIdInt int userId) {
-        if (!mUserManager.exists(userId)) return false;
+    @NonNull
+    public boolean[] canPackageQuery(@NonNull String sourcePackageName,
+            @NonNull String[] targetPackageNames, @UserIdInt int userId) {
+        final int targetSize = targetPackageNames.length;
+        final boolean[] results = new boolean[targetSize];
+        if (!mUserManager.exists(userId)) {
+            return results;
+        }
         final int callingUid = Binder.getCallingUid();
         enforceCrossUserPermission(callingUid, userId, false /*requireFullPermission*/,
-                false /*checkShell*/, "may package query");
+                false /*checkShell*/, "can package query");
+
         final PackageStateInternal sourceSetting = getPackageStateInternal(sourcePackageName);
-        final PackageStateInternal targetSetting = getPackageStateInternal(targetPackageName);
-        boolean throwException = sourceSetting == null || targetSetting == null;
-        if (!throwException) {
-            final boolean filterSource =
-                    shouldFilterApplicationIncludingUninstalled(sourceSetting, callingUid, userId);
-            final boolean filterTarget =
-                    shouldFilterApplicationIncludingUninstalled(targetSetting, callingUid, userId);
-            // The caller must have visibility of the both packages
-            throwException = filterSource || filterTarget;
+        final PackageStateInternal[] targetSettings = new PackageStateInternal[targetSize];
+        // Throw exception if the caller without the visibility of source package
+        boolean throwException =
+                (sourceSetting == null || shouldFilterApplicationIncludingUninstalled(
+                        sourceSetting, callingUid, userId));
+        for (int i = 0; !throwException && i < targetSize; i++) {
+            targetSettings[i] = getPackageStateInternal(targetPackageNames[i]);
+            // Throw exception if the caller without the visibility of target package
+            throwException =
+                    (targetSettings[i] == null || shouldFilterApplicationIncludingUninstalled(
+                            targetSettings[i], callingUid, userId));
         }
         if (throwException) {
             throw new ParcelableException(new PackageManager.NameNotFoundException("Package(s) "
-                    + sourcePackageName + " and/or " + targetPackageName + " not found."));
+                    + sourcePackageName + " and/or " + Arrays.toString(targetPackageNames)
+                    + " not found."));
         }
+
         final int sourcePackageUid = UserHandle.getUid(userId, sourceSetting.getAppId());
-        return !shouldFilterApplication(targetSetting, sourcePackageUid, userId);
+        for (int i = 0; i < targetSize; i++) {
+            results[i] = !shouldFilterApplication(targetSettings[i], sourcePackageUid, userId);
+        }
+        return results;
     }
 
     /*
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentFilterHelper.java b/services/core/java/com/android/server/pm/CrossProfileIntentFilterHelper.java
new file mode 100644
index 0000000..e682586
--- /dev/null
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentFilterHelper.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
+import android.util.ArraySet;
+
+/**
+ * Helper class to manage {@link com.android.server.pm.CrossProfileIntentFilter}s.
+ */
+public class CrossProfileIntentFilterHelper {
+    private Context mContext;
+    private UserManagerInternal mUserManagerInternal;
+    private Settings mSettings;
+    private UserManagerService mUserManagerService;
+    private PackageManagerTracedLock mLock;
+
+    public CrossProfileIntentFilterHelper(Settings settings, UserManagerService userManagerService,
+            PackageManagerTracedLock lock, UserManagerInternal userManagerInternal,
+            Context context) {
+        mSettings = settings;
+        mUserManagerService = userManagerService;
+        mLock = lock;
+        mContext = context;
+        mUserManagerInternal = userManagerInternal;
+    }
+
+    /**
+     * For users that have
+     * {@link android.content.pm.UserProperties#getUpdateCrossProfileIntentFiltersOnOTA} set, this
+     * task will update default {@link com.android.server.pm.CrossProfileIntentFilter} between that
+     * user and its parent. This will only update CrossProfileIntentFilters set by system package.
+     * The new default are configured in {@link UserTypeDetails}.
+     */
+    public void updateDefaultCrossProfileIntentFilter() {
+        for (UserInfo userInfo : mUserManagerInternal.getUsers(false)) {
+
+            UserProperties currentUserProperties = mUserManagerInternal
+                    .getUserProperties(userInfo.id);
+
+            if (currentUserProperties.getUpdateCrossProfileIntentFiltersOnOTA()) {
+                int parentUserId = mUserManagerInternal.getProfileParentId(userInfo.id);
+                if (parentUserId != userInfo.id) {
+                    clearCrossProfileIntentFilters(userInfo.id,
+                            mContext.getOpPackageName(), parentUserId);
+                    clearCrossProfileIntentFilters(parentUserId,
+                            mContext.getOpPackageName(),  userInfo.id);
+
+                    mUserManagerInternal.setDefaultCrossProfileIntentFilters(parentUserId,
+                            userInfo.id);
+                }
+            }
+        }
+    }
+
+    /**
+     * Clear {@link CrossProfileIntentFilter}s configured on source user by ownerPackage
+     * targeting the targetUserId. If targetUserId is null then it will clear
+     * {@link CrossProfileIntentFilter} for any target user.
+     * @param sourceUserId source user for whom CrossProfileIntentFilter would be configured
+     * @param ownerPackage package who would have configured CrossProfileIntentFilter
+     * @param targetUserId user id for which CrossProfileIntentFilter will be removed.
+     *                     This can be null in which case it will clear for any target user.
+     */
+    public void clearCrossProfileIntentFilters(int sourceUserId, String ownerPackage,
+            Integer targetUserId) {
+        synchronized (mLock) {
+            CrossProfileIntentResolver resolver = mSettings
+                    .editCrossProfileIntentResolverLPw(sourceUserId);
+            ArraySet<CrossProfileIntentFilter> set =
+                    new ArraySet<>(resolver.filterSet());
+            for (CrossProfileIntentFilter filter : set) {
+                //Only remove if calling user is allowed based on access control of
+                // {@link CrossProfileIntentFilter}
+                if (filter.getOwnerPackage().equals(ownerPackage)
+                        && (targetUserId == null || filter.mTargetUserId == targetUserId)
+                        && mUserManagerService.isCrossProfileIntentFilterAccessible(sourceUserId,
+                        filter.mTargetUserId, /* addCrossProfileIntentFilter */ false)) {
+                    resolver.removeFilter(filter);
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 0066592..5661399 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -62,6 +62,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.art.ArtManagerLocal;
+import com.android.server.art.DexUseManagerLocal;
 import com.android.server.art.model.ArtFlags;
 import com.android.server.art.model.OptimizeParams;
 import com.android.server.art.model.OptimizeResult;
@@ -932,9 +933,23 @@
     }
 
     /**
-     * Returns {@link ArtManagerLocal} if one is found and should be used for package optimization.
+     * Returns {@link DexUseManagerLocal} if ART Service should be used for package optimization.
      */
-    private @Nullable ArtManagerLocal getArtManagerLocal() {
+    public static @Nullable DexUseManagerLocal getDexUseManagerLocal() {
+        if (!"true".equals(SystemProperties.get("dalvik.vm.useartservice", ""))) {
+            return null;
+        }
+        try {
+            return LocalManagerRegistry.getManagerOrThrow(DexUseManagerLocal.class);
+        } catch (ManagerNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Returns {@link ArtManagerLocal} if ART Service should be used for package optimization.
+     */
+    private static @Nullable ArtManagerLocal getArtManagerLocal() {
         if (!"true".equals(SystemProperties.get("dalvik.vm.useartservice", ""))) {
             return null;
         }
diff --git a/services/core/java/com/android/server/pm/GentleUpdateHelper.java b/services/core/java/com/android/server/pm/GentleUpdateHelper.java
index 247ac90..7cb096d 100644
--- a/services/core/java/com/android/server/pm/GentleUpdateHelper.java
+++ b/services/core/java/com/android/server/pm/GentleUpdateHelper.java
@@ -16,7 +16,12 @@
 
 package com.android.server.pm;
 
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+
 import android.annotation.WorkerThread;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
 import android.app.ActivityThread;
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
@@ -28,6 +33,9 @@
 import android.content.pm.PackageInstaller.InstallConstraintsResult;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
 import android.util.Slog;
 
 import java.util.ArrayDeque;
@@ -73,12 +81,24 @@
         public final List<String> packageNames;
         public final InstallConstraints constraints;
         public final CompletableFuture<InstallConstraintsResult> future;
+        private final long mFinishTime;
+
+        /**
+         * Note {@code timeoutMillis} will be clamped to 0 ~ one week to avoid overflow.
+         */
         PendingInstallConstraintsCheck(List<String> packageNames,
                 InstallConstraints constraints,
-                CompletableFuture<InstallConstraintsResult> future) {
+                CompletableFuture<InstallConstraintsResult> future,
+                long timeoutMillis) {
             this.packageNames = packageNames;
             this.constraints = constraints;
             this.future = future;
+
+            timeoutMillis = Math.max(0, Math.min(DateUtils.WEEK_IN_MILLIS, timeoutMillis));
+            mFinishTime = SystemClock.elapsedRealtime() + timeoutMillis;
+        }
+        public boolean isTimedOut() {
+            return SystemClock.elapsedRealtime() >= mFinishTime;
         }
     }
 
@@ -95,15 +115,31 @@
         mAppStateHelper = appStateHelper;
     }
 
+    void systemReady() {
+        var am = mContext.getSystemService(ActivityManager.class);
+        // Monitor top-visible apps
+        am.addOnUidImportanceListener(this::onUidImportance, IMPORTANCE_FOREGROUND);
+        // Monitor foreground apps
+        am.addOnUidImportanceListener(this::onUidImportance, IMPORTANCE_FOREGROUND_SERVICE);
+    }
+
     /**
      * Checks if install constraints are satisfied for the given packages.
      */
     CompletableFuture<InstallConstraintsResult> checkInstallConstraints(
-            List<String> packageNames, InstallConstraints constraints) {
+            List<String> packageNames, InstallConstraints constraints,
+            long timeoutMillis) {
         var future = new CompletableFuture<InstallConstraintsResult>();
         mHandler.post(() -> {
+            long clampedTimeoutMillis = timeoutMillis;
+            if (constraints.isRequireDeviceIdle()) {
+                // Device-idle-constraint is required. Clamp the timeout to ensure
+                // timeout-check happens after device-idle-check.
+                clampedTimeoutMillis = Math.max(timeoutMillis, PENDING_CHECK_MILLIS);
+            }
+
             var pendingCheck = new PendingInstallConstraintsCheck(
-                    packageNames, constraints, future);
+                    packageNames, constraints, future, clampedTimeoutMillis);
             if (constraints.isRequireDeviceIdle()) {
                 mPendingChecks.add(pendingCheck);
                 // JobScheduler doesn't provide queries about whether the device is idle.
@@ -113,8 +149,17 @@
                 scheduleIdleJob();
                 mHandler.postDelayed(() -> processPendingCheck(pendingCheck, false),
                         PENDING_CHECK_MILLIS);
-            } else {
-                processPendingCheck(pendingCheck, false);
+            } else if (!processPendingCheck(pendingCheck, false)) {
+                // Not resolved. Schedule a job for re-check
+                mPendingChecks.add(pendingCheck);
+                scheduleIdleJob();
+            }
+
+            if (!future.isDone()) {
+                // Ensure the pending check is resolved after timeout, no matter constraints
+                // satisfied or not.
+                mHandler.postDelayed(() -> processPendingCheck(pendingCheck, false),
+                        clampedTimeoutMillis);
             }
         });
         return future;
@@ -143,29 +188,77 @@
     }
 
     @WorkerThread
-    private void processPendingCheck(PendingInstallConstraintsCheck pendingCheck, boolean isIdle) {
+    private boolean areConstraintsSatisfied(List<String> packageNames,
+            InstallConstraints constraints, boolean isIdle) {
+        return (!constraints.isRequireDeviceIdle() || isIdle)
+                && (!constraints.isRequireAppNotForeground()
+                || !mAppStateHelper.hasForegroundApp(packageNames))
+                && (!constraints.isRequireAppNotInteracting()
+                || !mAppStateHelper.hasInteractingApp(packageNames))
+                && (!constraints.isRequireAppNotTopVisible()
+                || !mAppStateHelper.hasTopVisibleApp(packageNames))
+                && (!constraints.isRequireNotInCall()
+                || !mAppStateHelper.isInCall());
+    }
+
+    @WorkerThread
+    private boolean processPendingCheck(
+            PendingInstallConstraintsCheck pendingCheck, boolean isIdle) {
         var future = pendingCheck.future;
         if (future.isDone()) {
-            return;
+            return true;
         }
         var constraints = pendingCheck.constraints;
         var packageNames = mAppStateHelper.getDependencyPackages(pendingCheck.packageNames);
-        var constraintsSatisfied = (!constraints.isRequireDeviceIdle() || isIdle)
-                && (!constraints.isRequireAppNotForeground()
-                        || !mAppStateHelper.hasForegroundApp(packageNames))
-                && (!constraints.isRequireAppNotInteracting()
-                        || !mAppStateHelper.hasInteractingApp(packageNames))
-                && (!constraints.isRequireAppNotTopVisible()
-                        || !mAppStateHelper.hasTopVisibleApp(packageNames))
-                && (!constraints.isRequireNotInCall()
-                        || !mAppStateHelper.isInCall());
-        future.complete(new InstallConstraintsResult((constraintsSatisfied)));
+        var satisfied = areConstraintsSatisfied(packageNames, constraints, isIdle);
+        if (satisfied || pendingCheck.isTimedOut()) {
+            future.complete(new InstallConstraintsResult((satisfied)));
+            return true;
+        }
+        return false;
     }
 
     @WorkerThread
     private void processPendingChecksInIdle() {
-        while (!mPendingChecks.isEmpty()) {
-            processPendingCheck(mPendingChecks.remove(), true);
+        int size = mPendingChecks.size();
+        for (int i = 0; i < size; ++i) {
+            var pendingCheck = mPendingChecks.remove();
+            if (!processPendingCheck(pendingCheck, true)) {
+                // Not resolved. Put it back in the queue.
+                mPendingChecks.add(pendingCheck);
+            }
+        }
+        if (!mPendingChecks.isEmpty()) {
+            // Schedule a job for remaining pending checks
+            scheduleIdleJob();
+        }
+    }
+
+    @WorkerThread
+    private void onUidImportance(String packageName,
+            @RunningAppProcessInfo.Importance int importance) {
+        int size = mPendingChecks.size();
+        for (int i = 0; i < size; ++i) {
+            var pendingCheck = mPendingChecks.remove();
+            var dependencyPackages =
+                    mAppStateHelper.getDependencyPackages(pendingCheck.packageNames);
+            if (!dependencyPackages.contains(packageName)
+                    || !processPendingCheck(pendingCheck, false)) {
+                mPendingChecks.add(pendingCheck);
+            }
+        }
+        if (!mPendingChecks.isEmpty()) {
+            // Schedule a job for remaining pending checks
+            scheduleIdleJob();
+        }
+    }
+
+    private void onUidImportance(int uid, @RunningAppProcessInfo.Importance int importance) {
+        var pm = ActivityThread.getPackageManager();
+        try {
+            var packageName = pm.getNameForUid(uid);
+            mHandler.post(() -> onUidImportance(packageName, importance));
+        } catch (RemoteException ignore) {
         }
     }
 }
diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java
index 5c3890c..38efc10 100644
--- a/services/core/java/com/android/server/pm/IPackageManagerBase.java
+++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java
@@ -1153,9 +1153,10 @@
 
     @Override
     @Deprecated
-    public final boolean canPackageQuery(@NonNull String sourcePackageName,
-            @NonNull String targetPackageName, @UserIdInt int userId) {
-        return snapshot().canPackageQuery(sourcePackageName, targetPackageName, userId);
+    @NonNull
+    public final boolean[] canPackageQuery(@NonNull String sourcePackageName,
+            @NonNull String[] targetPackageNames, @UserIdInt int userId) {
+        return snapshot().canPackageQuery(sourcePackageName, targetPackageNames, userId);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 911cfbd..34bb6ec 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1872,7 +1872,7 @@
                 if (new File(signaturePath).exists()) {
                     // If signature is provided, enable fs-verity first so that the file can be
                     // measured for signature check below.
-                    VerityUtils.setUpFsverity(filePath, (byte[]) null);
+                    VerityUtils.setUpFsverity(filePath);
 
                     if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) {
                         throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
@@ -2385,7 +2385,7 @@
             for (String path : apkPaths) {
                 if (!VerityUtils.hasFsverity(path)) {
                     try {
-                        VerityUtils.setUpFsverity(path, (byte[]) null);
+                        VerityUtils.setUpFsverity(path);
                     } catch (IOException e) {
                         // There's nothing we can do if the setup failed. Since fs-verity is
                         // optional, just ignore the error for now.
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 4803c5e..9c60795 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -45,6 +45,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.InstallConstraints;
+import android.content.pm.PackageInstaller.InstallConstraintsResult;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageItemInfo;
@@ -91,7 +92,6 @@
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.ImageUtils;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.internal.util.Preconditions;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
@@ -123,6 +123,7 @@
 import java.util.Random;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
 import java.util.function.IntPredicate;
 import java.util.function.Supplier;
 
@@ -153,6 +154,8 @@
     private static final long MAX_HISTORICAL_SESSIONS = 1048576;
     /** Destroy sessions older than this on storage free request */
     private static final long MAX_SESSION_AGE_ON_LOW_STORAGE_MILLIS = 8 * DateUtils.HOUR_IN_MILLIS;
+    /** Maximum time to wait for install constraints to be satisfied */
+    private static final long MAX_INSTALL_CONSTRAINTS_TIMEOUT_MILLIS = DateUtils.WEEK_IN_MILLIS;
 
     /** Threshold of historical sessions size */
     private static final int HISTORICAL_SESSIONS_THRESHOLD = 500;
@@ -300,6 +303,7 @@
     public void systemReady() {
         mAppOps = mContext.getSystemService(AppOpsManager.class);
         mStagingManager.systemReady();
+        mGentleUpdateHelper.systemReady();
 
         synchronized (mSessions) {
             readSessionsLocked();
@@ -1248,12 +1252,11 @@
         }
     }
 
-    @Override
-    public void checkInstallConstraints(String installerPackageName, List<String> packageNames,
-            InstallConstraints constraints, RemoteCallback callback) {
-        Preconditions.checkArgument(packageNames != null);
-        Preconditions.checkArgument(constraints != null);
-        Preconditions.checkArgument(callback != null);
+    private CompletableFuture<InstallConstraintsResult> checkInstallConstraintsInternal(
+            String installerPackageName, List<String> packageNames,
+            InstallConstraints constraints, long timeoutMillis) {
+        Objects.requireNonNull(packageNames);
+        Objects.requireNonNull(constraints);
 
         final var snapshot = mPm.snapshotComputer();
         final int callingUid = Binder.getCallingUid();
@@ -1267,7 +1270,16 @@
             }
         }
 
-        var future = mGentleUpdateHelper.checkInstallConstraints(packageNames, constraints);
+        return mGentleUpdateHelper.checkInstallConstraints(
+                packageNames, constraints, timeoutMillis);
+    }
+
+    @Override
+    public void checkInstallConstraints(String installerPackageName, List<String> packageNames,
+            InstallConstraints constraints, RemoteCallback callback) {
+        Objects.requireNonNull(callback);
+        var future = checkInstallConstraintsInternal(
+                installerPackageName, packageNames, constraints, /*timeoutMillis=*/0);
         future.thenAccept(result -> {
             var b = new Bundle();
             b.putParcelable("result", result);
@@ -1276,6 +1288,27 @@
     }
 
     @Override
+    public void waitForInstallConstraints(String installerPackageName, List<String> packageNames,
+            InstallConstraints constraints, IntentSender callback, long timeoutMillis) {
+        Objects.requireNonNull(callback);
+        if (timeoutMillis < 0 || timeoutMillis > MAX_INSTALL_CONSTRAINTS_TIMEOUT_MILLIS) {
+            throw new IllegalArgumentException("Invalid timeoutMillis=" + timeoutMillis);
+        }
+        var future = checkInstallConstraintsInternal(
+                installerPackageName, packageNames, constraints, timeoutMillis);
+        future.thenAccept(result -> {
+            final var intent = new Intent();
+            intent.putExtra(Intent.EXTRA_PACKAGES, packageNames.toArray(new String[0]));
+            intent.putExtra(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS, constraints);
+            intent.putExtra(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS_RESULT, result);
+            try {
+                callback.sendIntent(mContext, 0, intent, null, null);
+            } catch (SendIntentException ignore) {
+            }
+        });
+    }
+
+    @Override
     public void registerCallback(IPackageInstallerCallback callback, int userId) {
         final Computer snapshot = mPm.snapshotComputer();
         snapshot.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false,
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 3983acf..9aaf685 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -125,7 +125,7 @@
 import android.os.incremental.PerUidReadTimeouts;
 import android.os.incremental.StorageHealthCheckParams;
 import android.os.storage.StorageManager;
-import android.provider.Settings.Secure;
+import android.provider.Settings.Global;
 import android.stats.devicepolicy.DevicePolicyEnums;
 import android.system.ErrnoException;
 import android.system.Int64Ref;
@@ -1842,7 +1842,8 @@
             assertNoWriteFileTransfersOpenLocked();
 
             final boolean isSecureFrpEnabled =
-                    (Secure.getInt(mContext.getContentResolver(), Secure.SECURE_FRP_MODE, 0) == 1);
+                    Global.getInt(mContext.getContentResolver(), Global.SECURE_FRP_MODE, 0) == 1;
+
             if (isSecureFrpEnabled
                     && !isSecureFrpInstallAllowed(mContext, Binder.getCallingUid())) {
                 throw new SecurityException("Can't install packages while in secure FRP");
@@ -2785,6 +2786,7 @@
         // Default to require only if existing base apk has fs-verity signature.
         mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled()
                 && params.mode == SessionParams.MODE_INHERIT_EXISTING
+                && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath())
                 && (new File(VerityUtils.getFsveritySignatureFilePath(
                         pkgInfo.applicationInfo.getBaseCodePath()))).exists();
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 91f7011..759ec67 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -193,6 +193,7 @@
 import com.android.server.SystemConfig;
 import com.android.server.Watchdog;
 import com.android.server.apphibernation.AppHibernationManagerInternal;
+import com.android.server.art.DexUseManagerLocal;
 import com.android.server.compat.CompatChange;
 import com.android.server.compat.PlatformCompat;
 import com.android.server.pm.Installer.InstallerException;
@@ -212,6 +213,7 @@
 import com.android.server.pm.permission.PermissionManagerService;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.pkg.PackageUserState;
 import com.android.server.pm.pkg.PackageUserStateInternal;
@@ -1537,7 +1539,10 @@
                 (i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager(), pm),
                 (i, pm) -> IBackupManager.Stub.asInterface(ServiceManager.getService(
                         Context.BACKUP_SERVICE)),
-                (i, pm) -> new SharedLibrariesImpl(pm, i));
+                (i, pm) -> new SharedLibrariesImpl(pm, i),
+                (i, pm) -> new CrossProfileIntentFilterHelper(i.getSettings(),
+                        i.getUserManagerService(), i.getLock(), i.getUserManagerInternal(),
+                        context));
 
         if (Build.VERSION.SDK_INT <= 0) {
             Slog.w(TAG, "**** ro.build.version.sdk not set!");
@@ -2029,13 +2034,21 @@
             final WatchedArrayMap<String, PackageSetting> packageSettings =
                 mSettings.getPackagesLocked();
 
-            // Save the names of pre-existing packages prior to scanning, so we can determine
-            // which system packages are completely new due to an upgrade.
             if (isDeviceUpgrading()) {
+                // Save the names of pre-existing packages prior to scanning, so we can determine
+                // which system packages are completely new due to an upgrade.
                 mExistingPackages = new ArraySet<>(packageSettings.size());
                 for (PackageSetting ps : packageSettings.values()) {
                     mExistingPackages.add(ps.getPackageName());
                 }
+
+                // Triggering {@link com.android.server.pm.crossprofile.
+                // CrossProfileIntentFilterHelper.updateDefaultCrossProfileIntentFilter} to update
+                // {@link  CrossProfileIntentFilter}s between eligible users and their parent
+                t.traceBegin("cross profile intent filter update");
+                mInjector.getCrossProfileIntentFilterHelper()
+                        .updateDefaultCrossProfileIntentFilter();
+                t.traceEnd();
             }
 
             mCacheDir = PackageManagerServiceUtils.preparePackageParserCache(
@@ -3255,6 +3268,7 @@
         return isPackageDeviceAdmin(packageName, UserHandle.USER_ALL);
     }
 
+    // TODO(b/261957226): centralise this logic in DPM
     boolean isPackageDeviceAdmin(String packageName, int userId) {
         final IDevicePolicyManager dpm = getDevicePolicyManager();
         try {
@@ -3281,6 +3295,9 @@
                     if (dpm.packageHasActiveAdmins(packageName, users[i])) {
                         return true;
                     }
+                    if (isDeviceManagementRoleHolder(packageName, users[i])) {
+                        return true;
+                    }
                 }
             }
         } catch (RemoteException e) {
@@ -3288,6 +3305,24 @@
         return false;
     }
 
+    private boolean isDeviceManagementRoleHolder(String packageName, int userId) {
+        return Objects.equals(packageName, getDevicePolicyManagementRoleHolderPackageName(userId));
+    }
+
+    @Nullable
+    private String getDevicePolicyManagementRoleHolderPackageName(int userId) {
+        return Binder.withCleanCallingIdentity(() -> {
+            RoleManager roleManager = mContext.getSystemService(RoleManager.class);
+            List<String> roleHolders =
+                    roleManager.getRoleHoldersAsUser(
+                            RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT, UserHandle.of(userId));
+            if (roleHolders.isEmpty()) {
+                return null;
+            }
+            return roleHolders.get(0);
+        });
+    }
+
     /** Returns the device policy manager interface. */
     private IDevicePolicyManager getDevicePolicyManager() {
         if (mDevicePolicyManager == null) {
@@ -3448,6 +3483,7 @@
         scheduleWritePackageRestrictions(sourceUserId);
     }
 
+
     // Enforcing that callingUid is owning pkg on userId
     private void enforceOwnerRights(@NonNull Computer snapshot, String pkg, int callingUid) {
         // The system owns everything.
@@ -4638,21 +4674,9 @@
             enforceOwnerRights(snapshot, ownerPackage, callingUid);
             PackageManagerServiceUtils.enforceShellRestriction(mInjector.getUserManagerInternal(),
                     UserManager.DISALLOW_DEBUGGING_FEATURES, callingUid, sourceUserId);
-            synchronized (mLock) {
-                CrossProfileIntentResolver resolver =
-                        mSettings.editCrossProfileIntentResolverLPw(sourceUserId);
-                ArraySet<CrossProfileIntentFilter> set =
-                        new ArraySet<>(resolver.filterSet());
-                for (CrossProfileIntentFilter filter : set) {
-                    //Only remove if calling user is allowed based on access control of
-                    // {@link CrossProfileIntentFilter}
-                    if (filter.getOwnerPackage().equals(ownerPackage)
-                            && mUserManager.isCrossProfileIntentFilterAccessible(sourceUserId,
-                            filter.mTargetUserId, /* addCrossProfileIntentFilter */ false)) {
-                        resolver.removeFilter(filter);
-                    }
-                }
-            }
+            PackageManagerService.this.mInjector.getCrossProfileIntentFilterHelper()
+                            .clearCrossProfileIntentFilters(sourceUserId, ownerPackage,
+                                    null);
             scheduleWritePackageRestrictions(sourceUserId);
         }
 
@@ -5308,18 +5332,70 @@
                 return;
             }
 
-            // TODO(b/254043366): Call `ArtManagerLocal.notifyDexLoad`.
+            UserHandle user = Binder.getCallingUserHandle();
+            int userId = user.getIdentifier();
 
-            int userId = UserHandle.getCallingUserId();
-            ApplicationInfo ai =
-                    snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId);
-            if (ai == null) {
-                Slog.w(PackageManagerService.TAG, "Loading a package that does not exist for the calling user. package="
-                        + loadingPackageName + ", user=" + userId);
-                return;
+            // Proxy the call to either ART Service or the legacy implementation. If the
+            // implementation is switched with the system property, the dex usage info will be
+            // incomplete, with these effects:
+            //
+            // -  Shared dex files may temporarily get compiled for private use.
+            // -  Secondary dex files may not get compiled at all.
+            // -  Stale compiled artifacts for secondary dex files may not get cleaned up.
+            //
+            // This recovers in the first background dexopt after the depending apps have been
+            // loaded for the first time.
+
+            DexUseManagerLocal dexUseManager = DexOptHelper.getDexUseManagerLocal();
+            if (dexUseManager != null) {
+                // TODO(chiuwinson): Retrieve filtered snapshot from Computer instance instead.
+                try (PackageManagerLocal.FilteredSnapshot filteredSnapshot =
+                                LocalManagerRegistry.getManager(PackageManagerLocal.class)
+                                        .withFilteredSnapshot(callingUid, user)) {
+                    if (loaderIsa != null) {
+                        // Check that loaderIsa agrees with the ISA that dexUseManager will
+                        // determine.
+                        PackageState loadingPkgState =
+                                filteredSnapshot.getPackageState(loadingPackageName);
+                        // If we don't find the loading package just pass it through and let
+                        // dexUseManager throw on it.
+                        if (loadingPkgState != null) {
+                            String loadingPkgAbi = loadingPkgState.getPrimaryCpuAbi();
+                            if (loadingPkgAbi == null) {
+                                loadingPkgAbi = Build.SUPPORTED_ABIS[0];
+                            }
+                            String loadingPkgDexCodeIsa = InstructionSets.getDexCodeInstructionSet(
+                                    VMRuntime.getInstructionSet(loadingPkgAbi));
+                            if (!loaderIsa.equals(loadingPkgDexCodeIsa)) {
+                                // TODO(b/251903639): Make this crash to surface this problem
+                                // better.
+                                Slog.w(PackageManagerService.TAG,
+                                        "Invalid loaderIsa in notifyDexLoad call from "
+                                                + loadingPackageName + ", uid " + callingUid
+                                                + ": expected " + loadingPkgDexCodeIsa + ", got "
+                                                + loaderIsa);
+                                return;
+                            }
+                        }
+                    }
+
+                    // This is called from binder, so exceptions thrown here are caught and handled
+                    // by it.
+                    dexUseManager.notifyDexContainersLoaded(
+                            filteredSnapshot, loadingPackageName, classLoaderContextMap);
+                }
+            } else {
+                ApplicationInfo ai =
+                        snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId);
+                if (ai == null) {
+                    Slog.w(PackageManagerService.TAG,
+                            "Loading a package that does not exist for the calling user. package="
+                                    + loadingPackageName + ", user=" + userId);
+                    return;
+                }
+                mDexManager.notifyDexLoad(ai, classLoaderContextMap, loaderIsa, userId,
+                        Process.isIsolated(callingUid));
             }
-            mDexManager.notifyDexLoad(ai, classLoaderContextMap, loaderIsa, userId,
-                    Process.isIsolated(callingUid));
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
index 396994b..76e6e45f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
@@ -139,6 +139,7 @@
     private final Singleton<BackgroundDexOptService> mBackgroundDexOptService;
     private final Singleton<IBackupManager> mIBackupManager;
     private final Singleton<SharedLibrariesImpl> mSharedLibrariesProducer;
+    private final Singleton<CrossProfileIntentFilterHelper> mCrossProfileIntentFilterHelperProducer;
 
     PackageManagerServiceInjector(Context context, PackageManagerTracedLock lock,
             Installer installer, Object installLock, PackageAbiHelper abiHelper,
@@ -176,7 +177,8 @@
             ServiceProducer getSystemServiceProducer,
             Producer<BackgroundDexOptService> backgroundDexOptService,
             Producer<IBackupManager> iBackupManager,
-            Producer<SharedLibrariesImpl> sharedLibrariesProducer) {
+            Producer<SharedLibrariesImpl> sharedLibrariesProducer,
+            Producer<CrossProfileIntentFilterHelper> crossProfileIntentFilterHelperProducer) {
         mContext = context;
         mLock = lock;
         mInstaller = installer;
@@ -228,6 +230,8 @@
         mBackgroundDexOptService = new Singleton<>(backgroundDexOptService);
         mIBackupManager = new Singleton<>(iBackupManager);
         mSharedLibrariesProducer = new Singleton<>(sharedLibrariesProducer);
+        mCrossProfileIntentFilterHelperProducer = new Singleton<>(
+                crossProfileIntentFilterHelperProducer);
     }
 
     /**
@@ -262,6 +266,14 @@
         return mLock;
     }
 
+    /**
+     * {@link CrossProfileIntentFilterHelper} which manages {@link CrossProfileIntentFilter}
+     * @return CrossProfileIntentFilterHelper
+     */
+    public CrossProfileIntentFilterHelper getCrossProfileIntentFilterHelper() {
+        return mCrossProfileIntentFilterHelperProducer.get(this, mPackageManager);
+    }
+
     public Installer getInstaller() {
         return mInstaller;
     }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 01dee13..7fec0eb00f 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -369,8 +369,6 @@
 
     // Current settings file.
     private final File mSettingsFilename;
-    // Compressed current settings file.
-    private final File mCompressedSettingsFilename;
     // Previous settings file.
     // Removed when the current settings file successfully stored.
     private final File mPreviousSettingsFilename;
@@ -641,7 +639,6 @@
         mRuntimePermissionsPersistence = null;
         mPermissionDataProvider = null;
         mSettingsFilename = null;
-        mCompressedSettingsFilename = null;
         mPreviousSettingsFilename = null;
         mPackageListFilename = null;
         mStoppedPackagesFilename = null;
@@ -713,7 +710,6 @@
                 |FileUtils.S_IROTH|FileUtils.S_IXOTH,
                 -1, -1);
         mSettingsFilename = new File(mSystemDir, "packages.xml");
-        mCompressedSettingsFilename = new File(mSystemDir, "packages.compressed");
         mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
         mPackageListFilename = new File(mSystemDir, "packages.list");
         FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);
@@ -755,7 +751,6 @@
         mLock = null;
         mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence;
         mSettingsFilename = null;
-        mCompressedSettingsFilename = null;
         mPreviousSettingsFilename = null;
         mPackageListFilename = null;
         mStoppedPackagesFilename = null;
@@ -2597,8 +2592,6 @@
                 Slog.w(PackageManagerService.TAG, "Preserving older settings backup");
             }
         }
-        // Compressed settings are not valid anymore.
-        mCompressedSettingsFilename.delete();
 
         mPastSignatures.clear();
 
@@ -2688,30 +2681,10 @@
             mPreviousSettingsFilename.delete();
 
             FileUtils.setPermissions(mSettingsFilename.toString(),
-                    FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
+                    FileUtils.S_IRUSR|FileUtils.S_IWUSR
+                    |FileUtils.S_IRGRP|FileUtils.S_IWGRP,
                     -1, -1);
 
-            final FileInputStream fis = new FileInputStream(mSettingsFilename);
-            final AtomicFile compressed = new AtomicFile(mCompressedSettingsFilename);
-            final FileOutputStream fos = compressed.startWrite();
-
-            BackgroundThread.getHandler().post(() -> {
-                try {
-                    if (!nativeCompressLz4(fis.getFD().getInt$(), fos.getFD().getInt$())) {
-                        throw new IOException("Failed to compress");
-                    }
-                    compressed.finishWrite(fos);
-                    FileUtils.setPermissions(mCompressedSettingsFilename.toString(),
-                            FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP
-                                    | FileUtils.S_IWGRP, -1, -1);
-                } catch (IOException e) {
-                    Slog.e(PackageManagerService.TAG, "Failed to write compressed settings file: "
-                            + mCompressedSettingsFilename, e);
-                    compressed.delete();
-                }
-                IoUtils.closeQuietly(fis);
-            });
-
             writeKernelMappingLPr();
             writePackageListLPr();
             writeAllUsersPackageRestrictionsLPr(sync);
@@ -2734,8 +2707,6 @@
         //Debug.stopMethodTracing();
     }
 
-    private native boolean nativeCompressLz4(int inputFd, int outputFd);
-
     private void writeKernelRemoveUserLPr(int userId) {
         if (mKernelMappingFilename == null) return;
 
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 0a650c9..251da84 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -6986,10 +6986,8 @@
         @UserAssignmentResult
         public int assignUserToDisplayOnStart(@UserIdInt int userId,
                 @UserIdInt int profileGroupId, @UserStartMode int userStartMode, int displayId) {
-            // TODO(245939659): change UserVisibilityMediator to take @UserStartMode
-            boolean foreground = userStartMode == UserManagerInternal.USER_START_MODE_FOREGROUND;
             return mUserVisibilityMediator.assignUserToDisplayOnStart(userId, profileGroupId,
-                    foreground, displayId);
+                    userStartMode, displayId);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 8fb5773..2bb72b8 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -132,7 +132,8 @@
                         .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
                         .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_WITH_PARENT)
                         .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
-                        .setUseParentsContacts(true));
+                        .setUseParentsContacts(true)
+                        .setUpdateCrossProfileIntentFiltersOnOTA(true));
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index 92e8f55..40d87bc 100644
--- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -23,7 +23,11 @@
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND;
+import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND_VISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_START_MODE_FOREGROUND;
 import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString;
+import static com.android.server.pm.UserManagerInternal.userStartModeToString;
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
@@ -43,6 +47,7 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.am.EventLogTags;
 import com.android.server.pm.UserManagerInternal.UserAssignmentResult;
+import com.android.server.pm.UserManagerInternal.UserStartMode;
 import com.android.server.pm.UserManagerInternal.UserVisibilityListener;
 import com.android.server.utils.Slogf;
 
@@ -142,7 +147,8 @@
      * See {@link UserManagerInternal#assignUserToDisplayOnStart(int, int, int, int)}.
      */
     public @UserAssignmentResult int assignUserToDisplayOnStart(@UserIdInt int userId,
-            @UserIdInt int unResolvedProfileGroupId, boolean foreground, int displayId) {
+            @UserIdInt int unResolvedProfileGroupId, @UserStartMode int userStartMode,
+            int displayId) {
         Preconditions.checkArgument(!isSpecialUserId(userId), "user id cannot be generic: %d",
                 userId);
         // This method needs to perform 4 actions:
@@ -161,14 +167,16 @@
                 ? userId
                 : unResolvedProfileGroupId;
         if (DBG) {
-            Slogf.d(TAG, "assignUserToDisplayOnStart(%d, %d, %b, %d): actualProfileGroupId=%d",
-                    userId, unResolvedProfileGroupId, foreground, displayId, profileGroupId);
+            Slogf.d(TAG, "assignUserToDisplayOnStart(%d, %d, %s, %d): actualProfileGroupId=%d",
+                    userId, unResolvedProfileGroupId, userStartModeToString(userStartMode),
+                    displayId, profileGroupId);
         }
 
         int result;
         IntArray visibleUsersBefore, visibleUsersAfter;
         synchronized (mLock) {
-            result = getUserVisibilityOnStartLocked(userId, profileGroupId, foreground, displayId);
+            result = getUserVisibilityOnStartLocked(userId, profileGroupId, userStartMode,
+                    displayId);
             if (DBG) {
                 Slogf.d(TAG, "result of getUserVisibilityOnStartLocked(%s)",
                         userAssignmentResultToString(result));
@@ -185,7 +193,7 @@
             visibleUsersBefore = getVisibleUsers();
 
             // Set current user / profiles state
-            if (foreground) {
+            if (userStartMode == USER_START_MODE_FOREGROUND) {
                 mCurrentUserId = userId;
             }
             if (DBG) {
@@ -228,8 +236,23 @@
 
     @GuardedBy("mLock")
     @UserAssignmentResult
-    private int getUserVisibilityOnStartLocked(@UserIdInt int userId,
-            @UserIdInt int profileGroupId, boolean foreground, int displayId) {
+    private int getUserVisibilityOnStartLocked(@UserIdInt int userId, @UserIdInt int profileGroupId,
+            @UserStartMode int userStartMode, int displayId) {
+
+        // Check for invalid combinations first
+        if (userStartMode == USER_START_MODE_BACKGROUND && displayId != DEFAULT_DISPLAY) {
+            Slogf.wtf(TAG, "cannot start user (%d) as BACKGROUND_USER on secondary display (%d) "
+                    + "(it should be BACKGROUND_USER_VISIBLE", userId, displayId);
+            return USER_ASSIGNMENT_RESULT_FAILURE;
+        }
+        if (userStartMode == USER_START_MODE_BACKGROUND_VISIBLE
+                && displayId == DEFAULT_DISPLAY && !isProfile(userId, profileGroupId)) {
+            Slogf.wtf(TAG, "cannot start full user (%d) visible on default display", userId);
+            return USER_ASSIGNMENT_RESULT_FAILURE;
+        }
+
+        boolean foreground = userStartMode == USER_START_MODE_FOREGROUND;
+
         if (displayId != DEFAULT_DISPLAY) {
             if (foreground) {
                 Slogf.w(TAG, "getUserVisibilityOnStartLocked(%d, %d, %b, %d) failed: cannot start "
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7919e19..025348b 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -181,6 +181,7 @@
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 import android.view.autofill.AutofillManagerInternal;
+import android.widget.Toast;
 
 import com.android.internal.R;
 import com.android.internal.accessibility.AccessibilityShortcutController;
@@ -204,6 +205,7 @@
 import com.android.server.GestureLauncherService;
 import com.android.server.LocalServices;
 import com.android.server.SystemServiceManager;
+import com.android.server.UiThread;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.policy.KeyCombinationManager.TwoKeysCombinationRule;
@@ -2900,6 +2902,11 @@
                     }
                 }
                 break;
+            case KeyEvent.KEYCODE_RECENT_APPS:
+                if (down && repeatCount == 0) {
+                    showRecentApps(false /* triggeredFromAltTab */);
+                }
+                return key_consumed;
             case KeyEvent.KEYCODE_APP_SWITCH:
                 if (!keyguardOn) {
                     if (down && repeatCount == 0) {
@@ -3217,8 +3224,23 @@
             boolean isEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
                     SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
                     SensorPrivacyManager.Sensors.MICROPHONE);
+
             mSensorPrivacyManager.setSensorPrivacy(SensorPrivacyManager.Sensors.MICROPHONE,
                     !isEnabled);
+
+            int toastTextResId;
+            if (isEnabled) {
+                toastTextResId = R.string.mic_access_on_toast;
+            } else {
+                toastTextResId = R.string.mic_access_off_toast;
+            }
+
+            Toast.makeText(
+                mContext,
+                UiThread.get().getLooper(),
+                mContext.getString(toastTextResId),
+                Toast.LENGTH_SHORT)
+                .show();
         }
     }
 
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 1fe82f4..09a7b29 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -25,7 +25,6 @@
 import android.content.Context;
 import android.content.IIntentReceiver;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.hardware.display.DisplayManagerInternal;
 import android.media.AudioManager;
 import android.media.Ringtone;
@@ -67,6 +66,7 @@
 import com.android.server.statusbar.StatusBarManagerInternal;
 
 import java.io.PrintWriter;
+import java.util.UUID;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -140,9 +140,8 @@
     private final NotifierHandler mHandler;
     private final Executor mBackgroundExecutor;
     private final Intent mScreenOnIntent;
-    private final Bundle mScreenOnOptions;
     private final Intent mScreenOffIntent;
-    private final Bundle mScreenOffOptions;
+    private final Bundle mScreenOnOffOptions;
 
     // True if the device should suspend when the screen is off due to proximity.
     private final boolean mSuspendWhenScreenOffDueToProximityConfig;
@@ -204,14 +203,11 @@
         mScreenOnIntent.addFlags(
                 Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND
                 | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
-        mScreenOnOptions = BroadcastOptions.makeRemovingMatchingFilter(
-                new IntentFilter(Intent.ACTION_SCREEN_OFF)).toBundle();
         mScreenOffIntent = new Intent(Intent.ACTION_SCREEN_OFF);
         mScreenOffIntent.addFlags(
                 Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND
                 | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
-        mScreenOffOptions = BroadcastOptions.makeRemovingMatchingFilter(
-                new IntentFilter(Intent.ACTION_SCREEN_ON)).toBundle();
+        mScreenOnOffOptions = createScreenOnOffBroadcastOptions();
 
         mSuspendWhenScreenOffDueToProximityConfig = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_suspendWhenScreenOffDueToProximity);
@@ -229,6 +225,25 @@
     }
 
     /**
+     * Create the {@link BroadcastOptions} bundle that will be used with sending the
+     * {@link Intent#ACTION_SCREEN_ON} and {@link Intent#ACTION_SCREEN_OFF} broadcasts.
+     */
+    private Bundle createScreenOnOffBroadcastOptions() {
+        final BroadcastOptions options = BroadcastOptions.makeBasic();
+        // This allows the broadcasting system to discard any older broadcasts
+        // waiting to be delivered to a process.
+        options.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+        // Set namespace and key to identify which older broadcasts can be discarded.
+        // We could use any strings here but namespace needs to be unlikely to be reused with in
+        // the system_server process, as that could result in potentially discarding some
+        // non-screen on/off related broadcast.
+        options.setDeliveryGroupMatchingKey(
+                UUID.randomUUID().toString(),
+                Intent.ACTION_SCREEN_ON);
+        return options.toBundle();
+    }
+
+    /**
      * Called when a wake lock is acquired.
      */
     public void onWakeLockAcquired(int flags, String tag, String packageName,
@@ -798,7 +813,7 @@
         if (mActivityManagerInternal.isSystemReady()) {
             final boolean ordered = !mActivityManagerInternal.isModernQueueEnabled();
             mActivityManagerInternal.broadcastIntent(mScreenOnIntent, mWakeUpBroadcastDone,
-                    null, ordered, UserHandle.USER_ALL, null, null, mScreenOnOptions);
+                    null, ordered, UserHandle.USER_ALL, null, null, mScreenOnOffOptions);
         } else {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 2, 1);
             sendNextBroadcast();
@@ -823,7 +838,7 @@
         if (mActivityManagerInternal.isSystemReady()) {
             final boolean ordered = !mActivityManagerInternal.isModernQueueEnabled();
             mActivityManagerInternal.broadcastIntent(mScreenOffIntent, mGoToSleepBroadcastDone,
-                    null, ordered, UserHandle.USER_ALL, null, null, mScreenOffOptions);
+                    null, ordered, UserHandle.USER_ALL, null, null, mScreenOnOffOptions);
         } else {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 3, 1);
             sendNextBroadcast();
diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java
index 1c4e143..522c6c8 100644
--- a/services/core/java/com/android/server/power/PowerGroup.java
+++ b/services/core/java/com/android/server/power/PowerGroup.java
@@ -407,16 +407,14 @@
         return mDisplayPowerRequest.policy;
     }
 
-    boolean updateLocked(float screenBrightnessOverride, boolean autoBrightness,
-            boolean useProximitySensor, boolean boostScreenBrightness, int dozeScreenState,
-            float dozeScreenBrightness, boolean overrideDrawWakeLock,
-            PowerSaveState powerSaverState, boolean quiescent, boolean dozeAfterScreenOff,
-            boolean bootCompleted, boolean screenBrightnessBoostInProgress,
-            boolean waitForNegativeProximity) {
+    boolean updateLocked(float screenBrightnessOverride, boolean useProximitySensor,
+            boolean boostScreenBrightness, int dozeScreenState, float dozeScreenBrightness,
+            boolean overrideDrawWakeLock, PowerSaveState powerSaverState, boolean quiescent,
+            boolean dozeAfterScreenOff, boolean bootCompleted,
+            boolean screenBrightnessBoostInProgress, boolean waitForNegativeProximity) {
         mDisplayPowerRequest.policy = getDesiredScreenPolicyLocked(quiescent, dozeAfterScreenOff,
                 bootCompleted, screenBrightnessBoostInProgress);
         mDisplayPowerRequest.screenBrightnessOverride = screenBrightnessOverride;
-        mDisplayPowerRequest.useAutoBrightness = autoBrightness;
         mDisplayPowerRequest.useProximitySensor = useProximitySensor;
         mDisplayPowerRequest.boostScreenBrightness = boostScreenBrightness;
 
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 6e3c827..b5ddc06 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -581,10 +581,6 @@
     private boolean mIsFaceDown = false;
     private long mLastFlipTime = 0L;
 
-    // The screen brightness mode.
-    // One of the Settings.System.SCREEN_BRIGHTNESS_MODE_* constants.
-    private int mScreenBrightnessModeSetting;
-
     // The screen brightness setting override from the window manager
     // to allow the current foreground activity to override the brightness.
     private float mScreenBrightnessOverrideFromWindowManager =
@@ -1457,10 +1453,6 @@
             mSystemProperties.set(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED, retailDemoValue);
         }
 
-        mScreenBrightnessModeSetting = Settings.System.getIntForUser(resolver,
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
-
         mDirty |= DIRTY_SETTINGS;
     }
 
@@ -3432,23 +3424,18 @@
                 final PowerGroup powerGroup = mPowerGroups.valueAt(idx);
                 final int groupId = powerGroup.getGroupId();
 
-                // Determine appropriate screen brightness and auto-brightness adjustments.
-                final boolean autoBrightness;
+                // Determine appropriate screen brightness.
                 final float screenBrightnessOverride;
                 if (!mBootCompleted) {
                     // Keep the brightness steady during boot. This requires the
                     // bootloader brightness and the default brightness to be identical.
-                    autoBrightness = false;
                     screenBrightnessOverride = mScreenBrightnessDefault;
                 } else if (isValidBrightness(mScreenBrightnessOverrideFromWindowManager)) {
-                    autoBrightness = false;
                     screenBrightnessOverride = mScreenBrightnessOverrideFromWindowManager;
                 } else {
-                    autoBrightness = (mScreenBrightnessModeSetting
-                            == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
                     screenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT;
                 }
-                boolean ready = powerGroup.updateLocked(screenBrightnessOverride, autoBrightness,
+                boolean ready = powerGroup.updateLocked(screenBrightnessOverride,
                         shouldUseProximitySensorLocked(), shouldBoostScreenBrightness(),
                         mDozeScreenStateOverrideFromDreamManager,
                         mDozeScreenBrightnessOverrideFromDreamManagerFloat,
@@ -3469,7 +3456,6 @@
                             powerGroup.getUserActivitySummaryLocked())
                             + ", mBootCompleted=" + mBootCompleted
                             + ", screenBrightnessOverride=" + screenBrightnessOverride
-                            + ", useAutoBrightness=" + autoBrightness
                             + ", mScreenBrightnessBoostInProgress="
                             + mScreenBrightnessBoostInProgress
                             + ", sQuiescent=" + sQuiescent);
@@ -4488,7 +4474,6 @@
                     + mMaximumScreenOffTimeoutFromDeviceAdmin + " (enforced="
                     + isMaximumScreenOffTimeoutFromDeviceAdminEnforcedLocked() + ")");
             pw.println("  mStayOnWhilePluggedInSetting=" + mStayOnWhilePluggedInSetting);
-            pw.println("  mScreenBrightnessModeSetting=" + mScreenBrightnessModeSetting);
             pw.println("  mScreenBrightnessOverrideFromWindowManager="
                     + mScreenBrightnessOverrideFromWindowManager);
             pw.println("  mUserActivityTimeoutOverrideFromWindowManager="
@@ -4866,9 +4851,6 @@
             proto.end(stayOnWhilePluggedInToken);
 
             proto.write(
-                    PowerServiceSettingsAndConfigurationDumpProto.SCREEN_BRIGHTNESS_MODE_SETTING,
-                    mScreenBrightnessModeSetting);
-            proto.write(
                     PowerServiceSettingsAndConfigurationDumpProto
                             .SCREEN_BRIGHTNESS_OVERRIDE_FROM_WINDOW_MANAGER,
                     mScreenBrightnessOverrideFromWindowManager);
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 2a88473..855fcaf 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -763,8 +763,6 @@
     @NonNull
     private final BatteryStatsHistory mHistory;
 
-    private BatteryStatsHistoryIterator mBatteryStatsHistoryIterator;
-
     int mStartCount;
 
     /**
@@ -11242,17 +11240,11 @@
         return mHistory.getHistoryUsedSize();
     }
 
-    @Override
-    public boolean startIteratingHistoryLocked() {
-        mBatteryStatsHistoryIterator = createBatteryStatsHistoryIterator();
-        return true;
-    }
-
     /**
      * Creates an iterator for battery stats history.
      */
-    @VisibleForTesting
-    public BatteryStatsHistoryIterator createBatteryStatsHistoryIterator() {
+    @Override
+    public BatteryStatsHistoryIterator iterateBatteryStatsHistory() {
         return mHistory.iterate();
     }
 
@@ -11277,17 +11269,6 @@
     }
 
     @Override
-    public boolean getNextHistoryLocked(HistoryItem out) {
-        return mBatteryStatsHistoryIterator.next(out);
-    }
-
-    @Override
-    public void finishIteratingHistoryLocked() {
-        mBatteryStatsHistoryIterator.close();
-        mBatteryStatsHistoryIterator = null;
-    }
-
-    @Override
     public int getStartCount() {
         return mStartCount;
     }
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index c192057..721872b 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -690,7 +690,7 @@
      */
     public void lockUser(int userId) {
         mLockPatternUtils.requireStrongAuth(
-                StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId);
+                StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, userId);
         try {
             WindowManagerGlobal.getWindowManagerService().lockNow(null);
         } catch (RemoteException e) {
@@ -2088,7 +2088,7 @@
             if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) {
                 if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout");
                 mLockPatternUtils.requireStrongAuth(
-                        mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, mUserId);
+                        mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId);
             }
             maybeLockScreen(mUserId);
         }
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 05df22f..3be16a1 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -26,6 +26,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
+import static android.net.vcn.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY;
 import static android.net.vcn.VcnManager.VCN_ERROR_CODE_CONFIG_ERROR;
 import static android.net.vcn.VcnManager.VCN_ERROR_CODE_INTERNAL_ERROR;
 import static android.net.vcn.VcnManager.VCN_ERROR_CODE_NETWORK_ERROR;
@@ -36,6 +37,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.net.ConnectivityDiagnosticsManager;
+import android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
 import android.net.ConnectivityManager;
 import android.net.InetAddresses;
 import android.net.IpPrefix;
@@ -50,6 +53,7 @@
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkProvider;
+import android.net.NetworkRequest;
 import android.net.NetworkScore;
 import android.net.RouteInfo;
 import android.net.TelephonyNetworkSpecifier;
@@ -546,6 +550,39 @@
         }
     }
 
+    /**
+     * Sent when there is a suspected data stall on a network
+     *
+     * <p>Only relevant in the Connected state.
+     *
+     * @param arg1 The "all" token; this signal is always honored.
+     * @param obj @NonNull An EventDataStallSuspectedInfo instance with relevant data.
+     */
+    private static final int EVENT_DATA_STALL_SUSPECTED = 13;
+
+    private static class EventDataStallSuspectedInfo implements EventInfo {
+        @NonNull public final Network network;
+
+        EventDataStallSuspectedInfo(@NonNull Network network) {
+            this.network = network;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(network);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (!(other instanceof EventDataStallSuspectedInfo)) {
+                return false;
+            }
+
+            final EventDataStallSuspectedInfo rhs = (EventDataStallSuspectedInfo) other;
+            return Objects.equals(network, rhs.network);
+        }
+    }
+
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     @NonNull
     final DisconnectedState mDisconnectedState = new DisconnectedState();
@@ -578,10 +615,13 @@
     @NonNull
     private final VcnUnderlyingNetworkControllerCallback mUnderlyingNetworkControllerCallback;
 
+    @NonNull private final VcnConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback;
+
     private final boolean mIsMobileDataEnabled;
 
     @NonNull private final IpSecManager mIpSecManager;
     @NonNull private final ConnectivityManager mConnectivityManager;
+    @NonNull private final ConnectivityDiagnosticsManager mConnectivityDiagnosticsManager;
 
     @Nullable private IpSecTunnelInterface mTunnelIface = null;
 
@@ -748,6 +788,20 @@
                         mUnderlyingNetworkControllerCallback);
         mIpSecManager = mVcnContext.getContext().getSystemService(IpSecManager.class);
         mConnectivityManager = mVcnContext.getContext().getSystemService(ConnectivityManager.class);
+        mConnectivityDiagnosticsManager =
+                mVcnContext.getContext().getSystemService(ConnectivityDiagnosticsManager.class);
+
+        mConnectivityDiagnosticsCallback = new VcnConnectivityDiagnosticsCallback();
+
+        if (mConnectionConfig.hasGatewayOption(
+                VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY)) {
+            final NetworkRequest diagRequest =
+                    new NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build();
+            mConnectivityDiagnosticsManager.registerConnectivityDiagnosticsCallback(
+                    diagRequest,
+                    new HandlerExecutor(new Handler(vcnContext.getLooper())),
+                    mConnectivityDiagnosticsCallback);
+        }
 
         addState(mDisconnectedState);
         addState(mDisconnectingState);
@@ -810,6 +864,9 @@
         mUnderlyingNetworkController.teardown();
 
         mGatewayStatusCallback.onQuit();
+
+        mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback(
+                mConnectivityDiagnosticsCallback);
     }
 
     /**
@@ -828,6 +885,20 @@
         sendMessageAndAcquireWakeLock(EVENT_SUBSCRIPTIONS_CHANGED, TOKEN_ALL);
     }
 
+    private class VcnConnectivityDiagnosticsCallback extends ConnectivityDiagnosticsCallback {
+        @Override
+        public void onDataStallSuspected(ConnectivityDiagnosticsManager.DataStallReport report) {
+            mVcnContext.ensureRunningOnLooperThread();
+
+            final Network network = report.getNetwork();
+            logInfo("Data stall suspected on " + network);
+            sendMessageAndAcquireWakeLock(
+                    EVENT_DATA_STALL_SUSPECTED,
+                    TOKEN_ALL,
+                    new EventDataStallSuspectedInfo(network));
+        }
+    }
+
     private class VcnUnderlyingNetworkControllerCallback
             implements UnderlyingNetworkControllerCallback {
         @Override
@@ -1367,7 +1438,8 @@
                 case EVENT_SUBSCRIPTIONS_CHANGED: // Fallthrough
                 case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: // Fallthrough
                 case EVENT_MIGRATION_COMPLETED: // Fallthrough
-                case EVENT_IKE_CONNECTION_INFO_CHANGED:
+                case EVENT_IKE_CONNECTION_INFO_CHANGED: // Fallthrough
+                case EVENT_DATA_STALL_SUSPECTED:
                     logUnexpectedEvent(msg.what);
                     break;
                 default:
@@ -1925,6 +1997,11 @@
                     mIkeConnectionInfo =
                             ((EventIkeConnectionInfoChangedInfo) msg.obj).ikeConnectionInfo;
                     break;
+                case EVENT_DATA_STALL_SUSPECTED:
+                    final Network networkWithDataStall =
+                            ((EventDataStallSuspectedInfo) msg.obj).network;
+                    handleDataStallSuspected(networkWithDataStall);
+                    break;
                 default:
                     logUnhandledMessage(msg);
                     break;
@@ -1985,6 +2062,15 @@
             }
         }
 
+        private void handleDataStallSuspected(Network networkWithDataStall) {
+            if (mUnderlying != null
+                    && mNetworkAgent != null
+                    && mNetworkAgent.getNetwork().equals(networkWithDataStall)) {
+                logInfo("Perform Mobility update to recover from suspected data stall");
+                mIkeSession.setNetwork(mUnderlying.network);
+            }
+        }
+
         protected void setupInterfaceAndNetworkAgent(
                 int token,
                 @NonNull IpSecTunnelInterface tunnelIface,
@@ -2424,6 +2510,11 @@
     }
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
+    ConnectivityDiagnosticsCallback getConnectivityDiagnosticsCallback() {
+        return mConnectivityDiagnosticsCallback;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
     UnderlyingNetworkRecord getUnderlyingNetwork() {
         return mUnderlying;
     }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 4480d52..37450ac 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -603,6 +603,13 @@
      * for display.
      */
     void generateCrop(WallpaperData wallpaper) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("WPMS.generateCrop");
+        generateCropInternal(wallpaper);
+        t.traceEnd();
+    }
+
+    private void generateCropInternal(WallpaperData wallpaper) {
         boolean success = false;
 
         // Only generate crop for default display.
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index af430f9..2b49a81 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -17,7 +17,15 @@
 package com.android.server.wm;
 
 import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
+import static android.app.Activity.FULLSCREEN_MODE_REQUEST_ENTER;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
+import static android.app.FullscreenRequestHandler.REMOTE_CALLBACK_RESULT_KEY;
+import static android.app.FullscreenRequestHandler.RESULT_APPROVED;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_DEFAULT_FREEFORM;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_IN_FREEFORM;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_TOP_FOCUSED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -27,6 +35,7 @@
 import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_APPLICATION;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
@@ -52,6 +61,7 @@
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
+import android.app.FullscreenRequestHandler;
 import android.app.IActivityClientController;
 import android.app.ICompatCameraControlCallback;
 import android.app.IRequestFinishCallback;
@@ -71,6 +81,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.Parcel;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
@@ -85,6 +96,7 @@
 
 import com.android.internal.app.AssistUtils;
 import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.LocalServices;
@@ -675,29 +687,32 @@
 
     @Override
     public int getLaunchedFromUid(IBinder token) {
-        if (!canGetLaunchedFrom()) {
-            return INVALID_UID;
-        }
+        final int uid = Binder.getCallingUid();
+        final boolean isInternalCaller = isInternalCallerGetLaunchedFrom(uid);
         synchronized (mGlobalLock) {
             final ActivityRecord r = ActivityRecord.forTokenLocked(token);
-            return r != null ? r.launchedFromUid : INVALID_UID;
+            if (r != null && (isInternalCaller || r.mShareIdentity || r.launchedFromUid == uid)) {
+                return r.launchedFromUid;
+            }
         }
+        return INVALID_UID;
     }
 
     @Override
     public String getLaunchedFromPackage(IBinder token) {
-        if (!canGetLaunchedFrom()) {
-            return null;
-        }
+        final int uid = Binder.getCallingUid();
+        final boolean isInternalCaller = isInternalCallerGetLaunchedFrom(uid);
         synchronized (mGlobalLock) {
             final ActivityRecord r = ActivityRecord.forTokenLocked(token);
-            return r != null ? r.launchedFromPackage : null;
+            if (r != null && (isInternalCaller || r.mShareIdentity || r.launchedFromUid == uid)) {
+                return r.launchedFromPackage;
+            }
         }
+        return null;
     }
 
-    /** Whether the caller can get the package or uid that launched its activity. */
-    private boolean canGetLaunchedFrom() {
-        final int uid = Binder.getCallingUid();
+    /** Whether the call to one of the getLaunchedFrom APIs is performed by an internal caller. */
+    private boolean isInternalCallerGetLaunchedFrom(int uid) {
         if (UserHandle.getAppId(uid) == SYSTEM_UID) {
             return true;
         }
@@ -1056,6 +1071,133 @@
         }
     }
 
+    private @FullscreenRequestHandler.RequestResult int validateMultiwindowFullscreenRequestLocked(
+            Task topFocusedRootTask, int fullscreenRequest, ActivityRecord requesterActivity) {
+        // If the mode is not by default freeform, the freeform will be a user-driven event.
+        if (topFocusedRootTask.getParent().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+            return RESULT_FAILED_NOT_DEFAULT_FREEFORM;
+        }
+        // If this is not coming from the currently top-most activity, reject the request.
+        if (requesterActivity != topFocusedRootTask.getTopMostActivity()) {
+            return RESULT_FAILED_NOT_TOP_FOCUSED;
+        }
+        if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) {
+            if (topFocusedRootTask.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+                return RESULT_FAILED_NOT_IN_FREEFORM;
+            }
+        } else {
+            if (topFocusedRootTask.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+                return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
+            }
+            if (topFocusedRootTask.mMultiWindowRestoreWindowingMode == INVALID_WINDOWING_MODE) {
+                return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
+            }
+        }
+        return RESULT_APPROVED;
+    }
+
+    @Override
+    public void requestMultiwindowFullscreen(IBinder callingActivity, int fullscreenRequest,
+            IRemoteCallback callback) {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                requestMultiwindowFullscreenLocked(callingActivity, fullscreenRequest, callback);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void requestMultiwindowFullscreenLocked(IBinder callingActivity, int fullscreenRequest,
+            IRemoteCallback callback) {
+        final ActivityRecord r = ActivityRecord.forTokenLocked(callingActivity);
+        if (r == null) {
+            return;
+        }
+
+        // If the shell transition is not enabled, just execute and done.
+        final TransitionController controller = r.mTransitionController;
+        if (!controller.isShellTransitionsEnabled()) {
+            final @FullscreenRequestHandler.RequestResult int validateResult;
+            final Task topFocusedRootTask;
+            topFocusedRootTask = mService.getTopDisplayFocusedRootTask();
+            validateResult = validateMultiwindowFullscreenRequestLocked(topFocusedRootTask,
+                    fullscreenRequest, r);
+            reportMultiwindowFullscreenRequestValidatingResult(callback, validateResult);
+            if (validateResult == RESULT_APPROVED) {
+                executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask);
+            }
+            return;
+        }
+        // Initiate the transition.
+        final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */, controller,
+                mService.mWindowManager.mSyncEngine);
+        if (mService.mWindowManager.mSyncEngine.hasActiveSync()) {
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+                    "Creating Pending Multiwindow Fullscreen Request: %s", transition);
+            mService.mWindowManager.mSyncEngine.queueSyncSet(
+                    () -> r.mTransitionController.moveToCollecting(transition),
+                    () -> {
+                        executeFullscreenRequestTransition(fullscreenRequest, callback, r,
+                                transition, true /* queued */);
+                    });
+        } else {
+            executeFullscreenRequestTransition(fullscreenRequest, callback, r, transition,
+                    false /* queued */);
+        }
+    }
+
+    private void executeFullscreenRequestTransition(int fullscreenRequest, IRemoteCallback callback,
+            ActivityRecord r, Transition transition, boolean queued) {
+        final @FullscreenRequestHandler.RequestResult int validateResult;
+        final Task topFocusedRootTask;
+        topFocusedRootTask = mService.getTopDisplayFocusedRootTask();
+        validateResult = validateMultiwindowFullscreenRequestLocked(topFocusedRootTask,
+                fullscreenRequest, r);
+        reportMultiwindowFullscreenRequestValidatingResult(callback, validateResult);
+        if (validateResult != RESULT_APPROVED) {
+            if (queued) {
+                transition.abort();
+            }
+            return;
+        }
+        r.mTransitionController.requestStartTransition(transition, topFocusedRootTask,
+                null /* remoteTransition */, null /* displayChange */);
+        executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask);
+        transition.setReady(topFocusedRootTask, true);
+    }
+
+    private static void reportMultiwindowFullscreenRequestValidatingResult(IRemoteCallback callback,
+            @FullscreenRequestHandler.RequestResult int result) {
+        if (callback == null) {
+            return;
+        }
+        Bundle res = new Bundle();
+        res.putInt(REMOTE_CALLBACK_RESULT_KEY, result);
+        try {
+            callback.sendResult(res);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "client throws an exception back to the server, ignore it");
+        }
+    }
+
+    private static void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) {
+        final int targetWindowingMode;
+        if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) {
+            requester.mMultiWindowRestoreWindowingMode =
+                    requester.getRequestedOverrideWindowingMode();
+            targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
+        } else {
+            targetWindowingMode = requester.mMultiWindowRestoreWindowingMode;
+            requester.mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE;
+        }
+        requester.setWindowingMode(targetWindowingMode);
+        if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+            requester.setBounds(null);
+        }
+    }
+
     @Override
     public void startLockTaskModeByToken(IBinder token) {
         synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index f725d1a..efd0ffa 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -670,7 +670,7 @@
     private boolean mCurrentLaunchCanTurnScreenOn = true;
 
     /** Whether our surface was set to be showing in the last call to {@link #prepareSurfaces} */
-    private boolean mLastSurfaceShowing = true;
+    private boolean mLastSurfaceShowing;
 
     /**
      * The activity is opaque and fills the entire space of this task.
@@ -873,6 +873,7 @@
     boolean mEnteringAnimation;
     boolean mOverrideTaskTransition;
     boolean mDismissKeyguard;
+    boolean mShareIdentity;
 
     /** True if the activity has reported stopped; False if the activity becomes visible. */
     boolean mAppStopped;
@@ -1233,8 +1234,8 @@
                 pw.println(prefix + "supportsEnterPipOnTaskSwitch: "
                         + supportsEnterPipOnTaskSwitch);
             }
-            if (info.getMaxAspectRatio() != 0) {
-                pw.println(prefix + "maxAspectRatio=" + info.getMaxAspectRatio());
+            if (getMaxAspectRatio() != 0) {
+                pw.println(prefix + "maxAspectRatio=" + getMaxAspectRatio());
             }
             final float minAspectRatio = getMinAspectRatio();
             if (minAspectRatio != 0) {
@@ -1574,6 +1575,7 @@
                 newParent.setResumedActivity(this, "onParentChanged");
                 mImeInsetsFrozenUntilStartInput = false;
             }
+            mLetterboxUiController.onActivityParentChanged(newParent);
         }
 
         if (rootTask != null && rootTask.topRunningActivity() == this) {
@@ -1997,6 +1999,7 @@
 
             mOverrideTaskTransition = options.getOverrideTaskTransition();
             mDismissKeyguard = options.getDismissKeyguard();
+            mShareIdentity = options.getShareIdentity();
         }
 
         ColorDisplayService.ColorDisplayServiceInternal cds = LocalServices.getService(
@@ -5470,7 +5473,8 @@
         // no animation but there will still be a transition set.
         // We still need to delay hiding the surface such that it
         // can be synchronized with showing the next surface in the transition.
-        if (!isVisible() && !delayed && !displayContent.mAppTransition.isTransitionSet()) {
+        if (!usingShellTransitions && !isVisible() && !delayed
+                && !displayContent.mAppTransition.isTransitionSet()) {
             SurfaceControl.openTransaction();
             try {
                 forAllWindows(win -> {
@@ -7400,6 +7404,11 @@
     }
 
     @Override
+    boolean showSurfaceOnCreation() {
+        return false;
+    }
+
+    @Override
     void prepareSurfaces() {
         final boolean show = isVisible() || isAnimating(PARENTS,
                 ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS);
@@ -7638,6 +7647,15 @@
     @Configuration.Orientation
     @Override
     int getRequestedConfigurationOrientation(boolean forDisplay) {
+        if (mLetterboxUiController.hasInheritedOrientation()) {
+            final RootDisplayArea root = getRootDisplayArea();
+            if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) {
+                return ActivityInfo.reverseOrientation(
+                        mLetterboxUiController.getInheritedOrientation());
+            } else {
+                return mLetterboxUiController.getInheritedOrientation();
+            }
+        }
         if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) {
             // We use Task here because we want to be consistent with what happens in
             // multi-window mode where other tasks orientations are ignored.
@@ -7765,6 +7783,9 @@
 
     @Nullable
     CompatDisplayInsets getCompatDisplayInsets() {
+        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+            return mLetterboxUiController.getInheritedCompatDisplayInsets();
+        }
         return mCompatDisplayInsets;
     }
 
@@ -7847,6 +7868,10 @@
 
     // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer.
     private void updateCompatDisplayInsets() {
+        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+            mCompatDisplayInsets =  mLetterboxUiController.getInheritedCompatDisplayInsets();
+            return;
+        }
         if (mCompatDisplayInsets != null || !shouldCreateCompatDisplayInsets()) {
             // The override configuration is set only once in size compatibility mode.
             return;
@@ -7908,6 +7933,9 @@
 
     @Override
     float getCompatScale() {
+        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+            return mLetterboxUiController.getInheritedSizeCompatScale();
+        }
         return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale();
     }
 
@@ -8017,6 +8045,16 @@
     }
 
     /**
+     * @return The orientation to use to understand if reachability is enabled.
+     */
+    @ActivityInfo.ScreenOrientation
+    int getOrientationForReachability() {
+        return mLetterboxUiController.hasInheritedLetterboxBehavior()
+                ? mLetterboxUiController.getInheritedOrientation()
+                : getRequestedConfigurationOrientation();
+    }
+
+    /**
      * Returns whether activity bounds are letterboxed.
      *
      * <p>Note that letterbox UI may not be shown even when this returns {@code true}. See {@link
@@ -8056,6 +8094,10 @@
         if (!ignoreVisibility && !mVisibleRequested) {
             return APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
         }
+        // TODO(b/256564921): Investigate if we need new metrics for translucent activities
+        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+            return mLetterboxUiController.getInheritedAppCompatState();
+        }
         if (mInSizeCompatModeForBounds) {
             return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
         }
@@ -8526,6 +8568,11 @@
     }
 
     private boolean isInSizeCompatModeForBounds(final Rect appBounds, final Rect containerBounds) {
+        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+            // To avoid wrong app behaviour, we decided to disable SCM when a translucent activity
+            // is letterboxed.
+            return false;
+        }
         final int appWidth = appBounds.width();
         final int appHeight = appBounds.height();
         final int containerAppWidth = containerBounds.width();
@@ -8546,10 +8593,11 @@
 
         // The rest of the condition is that only one side is smaller than the container, but it
         // still needs to exclude the cases where the size is limited by the fixed aspect ratio.
-        if (info.getMaxAspectRatio() > 0) {
+        final float maxAspectRatio = getMaxAspectRatio();
+        if (maxAspectRatio > 0) {
             final float aspectRatio = (0.5f + Math.max(appWidth, appHeight))
                     / Math.min(appWidth, appHeight);
-            if (aspectRatio >= info.getMaxAspectRatio()) {
+            if (aspectRatio >= maxAspectRatio) {
                 // The current size has reached the max aspect ratio.
                 return false;
             }
@@ -8771,7 +8819,7 @@
     // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer.
     private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds,
             Rect containingBounds, float desiredAspectRatio) {
-        final float maxAspectRatio = info.getMaxAspectRatio();
+        final float maxAspectRatio = getMaxAspectRatio();
         final Task rootTask = getRootTask();
         final float minAspectRatio = getMinAspectRatio();
         final TaskFragment organizedTf = getOrganizedTaskFragment();
@@ -8878,6 +8926,9 @@
      * Returns the min aspect ratio of this activity.
      */
     float getMinAspectRatio() {
+        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+            return mLetterboxUiController.getInheritedMinAspectRatio();
+        }
         if (info.applicationInfo == null) {
             return info.getMinAspectRatio();
         }
@@ -8922,11 +8973,18 @@
                 && parent.getWindowConfiguration().getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
     }
 
+    float getMaxAspectRatio() {
+        if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+            return mLetterboxUiController.getInheritedMaxAspectRatio();
+        }
+        return info.getMaxAspectRatio();
+    }
+
     /**
      * Returns true if the activity has maximum or minimum aspect ratio.
      */
     private boolean hasFixedAspectRatio() {
-        return info.getMaxAspectRatio() != 0 || getMinAspectRatio() != 0;
+        return getMaxAspectRatio() != 0 || getMinAspectRatio() != 0;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 2f4d154..2762c45 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1823,7 +1823,7 @@
         if (mPreferredTaskDisplayArea != null) {
             final DisplayContent displayContent = mRootWindowContainer.getDisplayContentOrCreate(
                     mPreferredTaskDisplayArea.getDisplayId());
-            if (displayContent != null && displayContent.mDwpcHelper.hasController()) {
+            if (displayContent != null) {
                 final int targetWindowingMode = (targetTask != null)
                         ? targetTask.getWindowingMode() : displayContent.getWindowingMode();
                 final int launchingFromDisplayId =
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 8a247cf..33c90a0 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -395,7 +395,7 @@
 
         final DisplayContent displayContent =
                 mRootWindowContainer.getDisplayContentOrCreate(displayId);
-        if (displayContent != null && displayContent.mDwpcHelper.hasController()) {
+        if (displayContent != null) {
             final ArrayList<ActivityInfo> activities = new ArrayList<>();
             if (activityInfo != null) {
                 activities.add(activityInfo);
@@ -405,10 +405,8 @@
                     activities.add(r.info);
                 });
             }
-            if (!displayContent.mDwpcHelper.canContainActivities(activities,
-                    displayContent.getWindowingMode())) {
-                return false;
-            }
+            return displayContent.mDwpcHelper.canContainActivities(activities,
+                        displayContent.getWindowingMode());
         }
 
         return true;
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index 6e23ed9..8d5d0d5 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.content.Context.MEDIA_PROJECTION_SERVICE;
 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
@@ -27,8 +28,10 @@
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.media.projection.MediaProjectionManager;
+import android.media.projection.IMediaProjectionManager;
 import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.provider.DeviceConfig;
 import android.view.ContentRecordingSession;
 import android.view.Display;
@@ -83,13 +86,7 @@
     private int mLastOrientation = ORIENTATION_UNDEFINED;
 
     ContentRecorder(@NonNull DisplayContent displayContent) {
-        this(displayContent, () -> {
-            MediaProjectionManager mpm = displayContent.mWmService.mContext.getSystemService(
-                    MediaProjectionManager.class);
-            if (mpm != null) {
-                mpm.stopActiveProjection();
-            }
-        });
+        this(displayContent, new RemoteMediaProjectionManagerWrapper());
     }
 
     @VisibleForTesting
@@ -445,6 +442,9 @@
                 .setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */)
                 .apply();
         mLastRecordedBounds = new Rect(recordedContentBounds);
+        // Request to notify the client about the resize.
+        mMediaProjectionManager.notifyActiveProjectionCapturedContentResized(
+                mLastRecordedBounds.width(), mLastRecordedBounds.height());
     }
 
     /**
@@ -503,6 +503,56 @@
 
     @VisibleForTesting interface MediaProjectionManagerWrapper {
         void stopActiveProjection();
+        void notifyActiveProjectionCapturedContentResized(int width, int height);
+    }
+
+    private static final class RemoteMediaProjectionManagerWrapper implements
+            MediaProjectionManagerWrapper {
+        @Nullable private IMediaProjectionManager mIMediaProjectionManager = null;
+
+        @Override
+        public void stopActiveProjection() {
+            fetchMediaProjectionManager();
+            if (mIMediaProjectionManager == null) {
+                return;
+            }
+            try {
+                mIMediaProjectionManager.stopActiveProjection();
+            } catch (RemoteException e) {
+                ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
+                        "Unable to tell MediaProjectionManagerService to stop the active "
+                                + "projection: %s",
+                        e);
+            }
+        }
+
+        @Override
+        public void notifyActiveProjectionCapturedContentResized(int width, int height) {
+            fetchMediaProjectionManager();
+            if (mIMediaProjectionManager == null) {
+                return;
+            }
+            try {
+                mIMediaProjectionManager.notifyActiveProjectionCapturedContentResized(width,
+                        height);
+            } catch (RemoteException e) {
+                ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
+                        "Unable to tell MediaProjectionManagerService about resizing the active "
+                                + "projection: %s",
+                        e);
+            }
+        }
+
+        private void fetchMediaProjectionManager() {
+            if (mIMediaProjectionManager != null) {
+                return;
+            }
+            IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
+            if (b == null) {
+                return;
+            }
+            mIMediaProjectionManager = IMediaProjectionManager.Stub.asInterface(b);
+        }
     }
 
     private boolean isRecordingContentTask() {
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 69fd00c..72ed108 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -22,12 +22,14 @@
 import android.content.pm.ActivityInfo;
 import android.os.UserHandle;
 import android.util.ArraySet;
+import android.util.Slog;
 import android.window.DisplayWindowPolicyController;
 
 import java.io.PrintWriter;
 import java.util.List;
 
 class DisplayWindowPolicyControllerHelper {
+    private static final String TAG = "DisplayWindowPolicyControllerHelper";
 
     private final DisplayContent mDisplayContent;
 
@@ -69,6 +71,17 @@
     public boolean canContainActivities(@NonNull List<ActivityInfo> activities,
             @WindowConfiguration.WindowingMode int windowingMode) {
         if (mDisplayWindowPolicyController == null) {
+            for (int i = activities.size() - 1; i >= 0; i--) {
+                final ActivityInfo aInfo = activities.get(i);
+                if (aInfo.requiredDisplayCategory != null) {
+                    Slog.e(TAG,
+                            String.format("Activity with requiredDisplayCategory='%s' cannot be"
+                                            + " displayed on display %d because that display does"
+                                            + " not have a matching category",
+                                    aInfo.requiredDisplayCategory, mDisplayContent.mDisplayId));
+                    return false;
+                }
+            }
             return true;
         }
         return mDisplayWindowPolicyController.canContainActivities(activities, windowingMode);
@@ -81,6 +94,14 @@
             @WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId,
             boolean isNewTask) {
         if (mDisplayWindowPolicyController == null) {
+            if (activityInfo.requiredDisplayCategory != null) {
+                Slog.e(TAG,
+                        String.format("Activity with requiredDisplayCategory='%s' cannot be"
+                                + " launched on display %d because that display does"
+                                + " not have a matching category",
+                                activityInfo.requiredDisplayCategory, mDisplayContent.mDisplayId));
+                return false;
+            }
             return true;
         }
         return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, windowingMode,
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index c19353c..127a7bf 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Color;
+import android.provider.DeviceConfig;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -103,6 +104,10 @@
 
     final Context mContext;
 
+    // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier
+    @NonNull
+    private final LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+
     // Aspect ratio of letterbox for fixed orientation, values <=
     // MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO will be ignored.
     private float mFixedOrientationLetterboxAspectRatio;
@@ -165,9 +170,12 @@
     // Whether using split screen aspect ratio as a default aspect ratio for unresizable apps.
     private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled;
 
-    // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier
-    @NonNull
-    private final LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+    // Whether letterboxing strategy is enabled for translucent activities. If {@value false}
+    // all the feature is disabled
+    private boolean mTranslucentLetterboxingEnabled;
+
+    // Allows to enable letterboxing strategy for translucent activities ignoring flags.
+    private boolean mTranslucentLetterboxingOverrideEnabled;
 
     LetterboxConfiguration(Context systemUiContext) {
         this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
@@ -206,6 +214,8 @@
                 R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps));
         mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
+        mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean(
+                R.bool.config_letterboxIsEnabledForTranslucentActivities);
         mLetterboxConfigurationPersister = letterboxConfigurationPersister;
         mLetterboxConfigurationPersister.start();
     }
@@ -817,6 +827,32 @@
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
     }
 
+    boolean isTranslucentLetterboxingEnabled() {
+        return mTranslucentLetterboxingOverrideEnabled || (mTranslucentLetterboxingEnabled
+                && isTranslucentLetterboxingAllowed());
+    }
+
+    void setTranslucentLetterboxingEnabled(boolean translucentLetterboxingEnabled) {
+        mTranslucentLetterboxingEnabled = translucentLetterboxingEnabled;
+    }
+
+    void setTranslucentLetterboxingOverrideEnabled(
+            boolean translucentLetterboxingOverrideEnabled) {
+        mTranslucentLetterboxingOverrideEnabled = translucentLetterboxingOverrideEnabled;
+        setTranslucentLetterboxingEnabled(translucentLetterboxingOverrideEnabled);
+    }
+
+    /**
+     * Resets whether we use the constraints override strategy for letterboxing when dealing
+     * with translucent activities {@link R.bool.config_letterboxIsEnabledForTranslucentActivities}.
+     */
+    void resetTranslucentLetterboxingEnabled() {
+        final boolean newValue = mContext.getResources().getBoolean(
+                R.bool.config_letterboxIsEnabledForTranslucentActivities);
+        setTranslucentLetterboxingEnabled(newValue);
+        setTranslucentLetterboxingOverrideEnabled(false);
+    }
+
     /** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */
     private void updatePositionForHorizontalReachability(
             Function<Integer, Integer> newHorizonalPositionFun) {
@@ -839,4 +875,9 @@
                 nextVerticalPosition);
     }
 
+    // TODO(b/262378106): Cache runtime flag and implement DeviceConfig.OnPropertiesChangedListener
+    static boolean isTranslucentLetterboxingAllowed() {
+        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+                "enable_translucent_activity_letterbox", false);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index bcea6f4..a53a5fc 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
@@ -27,6 +28,7 @@
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM;
 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT;
@@ -82,13 +84,44 @@
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
 
+    private static final float UNDEFINED_ASPECT_RATIO = 0f;
+
     private final Point mTmpPoint = new Point();
 
     private final LetterboxConfiguration mLetterboxConfiguration;
+
     private final ActivityRecord mActivityRecord;
 
+    /*
+     * WindowContainerListener responsible to make translucent activities inherit
+     * constraints from the first opaque activity beneath them. It's null for not
+     * translucent activities.
+     */
+    @Nullable
+    private WindowContainerListener mLetterboxConfigListener;
+
     private boolean mShowWallpaperForLetterboxBackground;
 
+    // In case of transparent activities we might need to access the aspectRatio of the
+    // first opaque activity beneath.
+    private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
+    private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
+
+    @Configuration.Orientation
+    private int mInheritedOrientation = Configuration.ORIENTATION_UNDEFINED;
+
+    // The app compat state for the opaque activity if any
+    private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
+
+    // If true it means that the opaque activity beneath a translucent one is in SizeCompatMode.
+    private boolean mIsInheritedInSizeCompatMode;
+
+    // This is the SizeCompatScale of the opaque activity beneath a translucent one
+    private float mInheritedSizeCompatScale;
+
+    // The CompatDisplayInsets of the opaque activity beneath the translucent one.
+    private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
+
     @Nullable
     private Letterbox mLetterbox;
 
@@ -220,7 +253,9 @@
                     : mActivityRecord.inMultiWindowMode()
                             ? mActivityRecord.getTask().getBounds()
                             : mActivityRecord.getRootTask().getParent().getBounds();
-            mLetterbox.layout(spaceToFill, w.getFrame(), mTmpPoint);
+            final Rect innerFrame = hasInheritedLetterboxBehavior()
+                    ? mActivityRecord.getWindowConfiguration().getBounds() : w.getFrame();
+            mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
         } else if (mLetterbox != null) {
             mLetterbox.hide();
         }
@@ -305,7 +340,9 @@
     }
 
     private void handleHorizontalDoubleTap(int x) {
-        if (!isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) {
+        // TODO(b/260857308): Investigate if enabling reachability for translucent activity
+        if (hasInheritedLetterboxBehavior() || !isHorizontalReachabilityEnabled()
+                || mActivityRecord.isInTransition()) {
             return;
         }
 
@@ -341,7 +378,9 @@
     }
 
     private void handleVerticalDoubleTap(int y) {
-        if (!isVerticalReachabilityEnabled() || mActivityRecord.isInTransition()) {
+        // TODO(b/260857308): Investigate if enabling reachability for translucent activity
+        if (hasInheritedLetterboxBehavior() || !isVerticalReachabilityEnabled()
+                || mActivityRecord.isInTransition()) {
             return;
         }
 
@@ -390,7 +429,7 @@
                 && parentConfiguration.windowConfiguration.getWindowingMode()
                         == WINDOWING_MODE_FULLSCREEN
                 && (parentConfiguration.orientation == ORIENTATION_LANDSCAPE
-                && mActivityRecord.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT);
+                && mActivityRecord.getOrientationForReachability() == ORIENTATION_PORTRAIT);
     }
 
     private boolean isHorizontalReachabilityEnabled() {
@@ -412,7 +451,7 @@
                 && parentConfiguration.windowConfiguration.getWindowingMode()
                         == WINDOWING_MODE_FULLSCREEN
                 && (parentConfiguration.orientation == ORIENTATION_PORTRAIT
-                && mActivityRecord.getRequestedConfigurationOrientation() == ORIENTATION_LANDSCAPE);
+                && mActivityRecord.getOrientationForReachability() == ORIENTATION_LANDSCAPE);
     }
 
     private boolean isVerticalReachabilityEnabled() {
@@ -576,9 +615,7 @@
         // Rounded corners should be displayed above the taskbar.
         bounds.bottom =
                 Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top);
-        if (mActivityRecord.inSizeCompatMode() && mActivityRecord.getCompatScale() < 1.0f) {
-            bounds.scale(1.0f / mActivityRecord.getCompatScale());
-        }
+        scaleIfNeeded(bounds);
     }
 
     private int getInsetsStateCornerRadius(
@@ -788,4 +825,144 @@
                 w.mAttrs.insetsFlags.appearance
         );
     }
+
+    /**
+     * Handles translucent activities letterboxing inheriting constraints from the
+     * first opaque activity beneath.
+     * @param parent The parent container.
+     */
+    void onActivityParentChanged(WindowContainer<?> parent) {
+        if (!mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
+            return;
+        }
+        if (mLetterboxConfigListener != null) {
+            mLetterboxConfigListener.onRemoved();
+            clearInheritedConfig();
+        }
+        // In case mActivityRecord.getCompatDisplayInsets() is not null we don't apply the
+        // opaque activity constraints because we're expecting the activity is already letterboxed.
+        if (mActivityRecord.getTask() == null || mActivityRecord.getCompatDisplayInsets() != null
+                || mActivityRecord.fillsParent()) {
+            return;
+        }
+        final ActivityRecord firstOpaqueActivityBeneath = mActivityRecord.getTask().getActivity(
+                ActivityRecord::fillsParent, mActivityRecord, false /* includeBoundary */,
+                true /* traverseTopToBottom */);
+        if (firstOpaqueActivityBeneath == null
+                || mActivityRecord.launchedFromUid != firstOpaqueActivityBeneath.getUid()) {
+            // We skip letterboxing if the translucent activity doesn't have any opaque
+            // activities beneath of if it's launched from a different user (e.g. notification)
+            return;
+        }
+        inheritConfiguration(firstOpaqueActivityBeneath);
+        mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation(
+                mActivityRecord, firstOpaqueActivityBeneath,
+                (opaqueConfig, transparentConfig) -> {
+                    final Configuration mutatedConfiguration = new Configuration();
+                    final Rect parentBounds = parent.getWindowConfiguration().getBounds();
+                    final Rect bounds = mutatedConfiguration.windowConfiguration.getBounds();
+                    final Rect letterboxBounds = opaqueConfig.windowConfiguration.getBounds();
+                    // We cannot use letterboxBounds directly here because the position relies on
+                    // letterboxing. Using letterboxBounds directly, would produce a double offset.
+                    bounds.set(parentBounds.left, parentBounds.top,
+                            parentBounds.left + letterboxBounds.width(),
+                            parentBounds.top + letterboxBounds.height());
+                    // We need to initialize appBounds to avoid NPE. The actual value will
+                    // be set ahead when resolving the Configuration for the activity.
+                    mutatedConfiguration.windowConfiguration.setAppBounds(new Rect());
+                    return mutatedConfiguration;
+                });
+    }
+
+    /**
+     * @return {@code true} if the current activity is translucent with an opaque activity
+     * beneath. In this case it will inherit bounds, orientation and aspect ratios from
+     * the first opaque activity beneath.
+     */
+    boolean hasInheritedLetterboxBehavior() {
+        return mLetterboxConfigListener != null && !mActivityRecord.matchParentBounds();
+    }
+
+    /**
+     * @return {@code true} if the current activity is translucent with an opaque activity
+     * beneath and needs to inherit its orientation.
+     */
+    boolean hasInheritedOrientation() {
+        // To force a different orientation, the transparent one needs to have an explicit one
+        // otherwise the existing one is fine and the actual orientation will depend on the
+        // bounds.
+        // To avoid wrong behaviour, we're not forcing orientation for activities with not
+        // fixed orientation (e.g. permission dialogs).
+        return hasInheritedLetterboxBehavior()
+                && mActivityRecord.mOrientation != SCREEN_ORIENTATION_UNSPECIFIED;
+    }
+
+    float getInheritedMinAspectRatio() {
+        return mInheritedMinAspectRatio;
+    }
+
+    float getInheritedMaxAspectRatio() {
+        return mInheritedMaxAspectRatio;
+    }
+
+    int getInheritedAppCompatState() {
+        return mInheritedAppCompatState;
+    }
+
+    float getInheritedSizeCompatScale() {
+        return mInheritedSizeCompatScale;
+    }
+
+    @Configuration.Orientation
+    int getInheritedOrientation() {
+        return mInheritedOrientation;
+    }
+
+    public ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
+        return mInheritedCompatDisplayInsets;
+    }
+
+    private void inheritConfiguration(ActivityRecord firstOpaque) {
+        // To avoid wrong behaviour, we're not forcing a specific aspet ratio to activities
+        // which are not already providing one (e.g. permission dialogs) and presumably also
+        // not resizable.
+        if (mActivityRecord.getMinAspectRatio() != UNDEFINED_ASPECT_RATIO) {
+            mInheritedMinAspectRatio = firstOpaque.getMinAspectRatio();
+        }
+        if (mActivityRecord.getMaxAspectRatio() != UNDEFINED_ASPECT_RATIO) {
+            mInheritedMaxAspectRatio = firstOpaque.getMaxAspectRatio();
+        }
+        mInheritedOrientation = firstOpaque.getRequestedConfigurationOrientation();
+        mInheritedAppCompatState = firstOpaque.getAppCompatState();
+        mIsInheritedInSizeCompatMode = firstOpaque.inSizeCompatMode();
+        mInheritedSizeCompatScale = firstOpaque.getCompatScale();
+        mInheritedCompatDisplayInsets = firstOpaque.getCompatDisplayInsets();
+    }
+
+    private void clearInheritedConfig() {
+        mLetterboxConfigListener = null;
+        mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
+        mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
+        mInheritedOrientation = Configuration.ORIENTATION_UNDEFINED;
+        mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
+        mIsInheritedInSizeCompatMode = false;
+        mInheritedSizeCompatScale = 1f;
+        mInheritedCompatDisplayInsets = null;
+    }
+
+    private void scaleIfNeeded(Rect bounds) {
+        if (boundsNeedToScale()) {
+            bounds.scale(1.0f / mActivityRecord.getCompatScale());
+        }
+    }
+
+    private boolean boundsNeedToScale() {
+        if (hasInheritedLetterboxBehavior()) {
+            return mIsInheritedInSizeCompatMode
+                    && mInheritedSizeCompatScale < 1.0f;
+        } else {
+            return mActivityRecord.inSizeCompatMode()
+                    && mActivityRecord.getCompatScale() < 1.0f;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 449e77f..d395f12 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -31,6 +31,7 @@
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+import static com.android.server.wm.utils.CoordinateTransforms.computeRotationMatrix;
 
 import android.animation.ArgbEvaluator;
 import android.content.Context;
@@ -60,7 +61,6 @@
 import com.android.server.display.DisplayControl;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
 import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
-import com.android.server.wm.utils.RotationAnimationUtils;
 
 import java.io.PrintWriter;
 
@@ -378,8 +378,7 @@
         // to the snapshot to make it stay in the same original position
         // with the current screen rotation.
         int delta = deltaRotation(rotation, mOriginalRotation);
-        RotationAnimationUtils.createRotationMatrix(delta, mOriginalWidth, mOriginalHeight,
-                mSnapshotInitialMatrix);
+        computeRotationMatrix(delta, mOriginalWidth, mOriginalHeight, mSnapshotInitialMatrix);
         setRotationTransform(t, mSnapshotInitialMatrix);
     }
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 03e3e93..07e3b83 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -18,6 +18,7 @@
 
 import static android.app.ActivityManager.isStartResultSuccessful;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
 import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED;
 import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
@@ -443,6 +444,8 @@
     @Surface.Rotation
     private int mRotation;
 
+    int mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE;
+
     /**
      * Last requested orientation reported to DisplayContent. This is different from {@link
      * #mOrientation} in the sense that this takes activities' requested orientation into
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 9126586..d1cc4b3 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2540,7 +2540,7 @@
                 mRemoteToken.toWindowContainerToken(),
                 getConfiguration(),
                 getNonFinishingActivityCount(),
-                isVisibleRequested(),
+                shouldBeVisible(null /* starting */),
                 childActivities,
                 positionInParent,
                 mClearedTaskForReuse,
@@ -2830,6 +2830,14 @@
         return getWindowingMode() == WINDOWING_MODE_FULLSCREEN || matchParentBounds();
     }
 
+    @Override
+    protected boolean onChildVisibleRequestedChanged(@Nullable WindowContainer child) {
+        if (!super.onChildVisibleRequestedChanged(child)) return false;
+        // Send the info changed to update the TaskFragment visibility.
+        sendTaskFragmentInfoChanged();
+        return true;
+    }
+
     @Nullable
     @Override
     TaskFragment getTaskFragment(Predicate<TaskFragment> callback) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 1d17cd4..64574a7 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -642,7 +642,6 @@
         if (showSurfaceOnCreation()) {
             getSyncTransaction().show(mSurfaceControl);
         }
-        onSurfaceShown(getSyncTransaction());
         updateSurfacePositionNonOrganized();
         if (mLastMagnificationSpec != null) {
             applyMagnificationSpec(getSyncTransaction(), mLastMagnificationSpec);
@@ -697,13 +696,6 @@
         scheduleAnimation();
     }
 
-    /**
-     * Called when the surface is shown for the first time.
-     */
-    void onSurfaceShown(Transaction t) {
-        // do nothing
-    }
-
     // Temp. holders for a chain of containers we are currently processing.
     private final LinkedList<WindowContainer> mTmpChain1 = new LinkedList<>();
     private final LinkedList<WindowContainer> mTmpChain2 = new LinkedList<>();
@@ -3989,27 +3981,54 @@
         unregisterConfigurationChangeListener(listener);
     }
 
+    static void overrideConfigurationPropagation(WindowContainer<?> receiver,
+            WindowContainer<?> supplier) {
+        overrideConfigurationPropagation(receiver, supplier, null /* configurationMerger */);
+    }
+
     /**
      * Forces the receiver container to always use the configuration of the supplier container as
      * its requested override configuration. It allows to propagate configuration without changing
      * the relationship between child and parent.
+     *
+     * @param receiver            The {@link WindowContainer<?>} which will receive the {@link
+     *                            Configuration} result of the merging operation.
+     * @param supplier            The {@link WindowContainer<?>} which provides the initial {@link
+     *                            Configuration}.
+     * @param configurationMerger A {@link ConfigurationMerger} which combines the {@link
+     *                            Configuration} of the receiver and the supplier.
      */
-    static void overrideConfigurationPropagation(WindowContainer<?> receiver,
-            WindowContainer<?> supplier) {
+    static WindowContainerListener overrideConfigurationPropagation(WindowContainer<?> receiver,
+            WindowContainer<?> supplier, @Nullable ConfigurationMerger configurationMerger) {
         final ConfigurationContainerListener listener = new ConfigurationContainerListener() {
             @Override
             public void onMergedOverrideConfigurationChanged(Configuration mergedOverrideConfig) {
-                receiver.onRequestedOverrideConfigurationChanged(supplier.getConfiguration());
+                final Configuration mergedConfiguration =
+                        configurationMerger != null
+                                ? configurationMerger.merge(mergedOverrideConfig,
+                                receiver.getConfiguration())
+                                : supplier.getConfiguration();
+                receiver.onRequestedOverrideConfigurationChanged(mergedConfiguration);
             }
         };
         supplier.registerConfigurationChangeListener(listener);
-        receiver.registerWindowContainerListener(new WindowContainerListener() {
+        final WindowContainerListener wcListener = new WindowContainerListener() {
             @Override
             public void onRemoved() {
                 receiver.unregisterWindowContainerListener(this);
                 supplier.unregisterConfigurationChangeListener(listener);
             }
-        });
+        };
+        receiver.registerWindowContainerListener(wcListener);
+        return wcListener;
+    }
+
+    /**
+     * Abstraction for functions merging two {@link Configuration} objects into one.
+     */
+    @FunctionalInterface
+    interface ConfigurationMerger {
+        Configuration merge(Configuration first, Configuration second);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 46a30fb..060784d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -978,6 +978,29 @@
         return 0;
     }
 
+    private int runSetTranslucentLetterboxingEnabled(PrintWriter pw) {
+        String arg = getNextArg();
+        final boolean enabled;
+        switch (arg) {
+            case "true":
+            case "1":
+                enabled = true;
+                break;
+            case "false":
+            case "0":
+                enabled = false;
+                break;
+            default:
+                getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
+                return -1;
+        }
+
+        synchronized (mInternal.mGlobalLock) {
+            mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(enabled);
+        }
+        return 0;
+    }
+
     private int runSetLetterboxStyle(PrintWriter pw) throws RemoteException {
         if (peekNextArg() == null) {
             getErrPrintWriter().println("Error: No arguments provided.");
@@ -1033,6 +1056,9 @@
                 case "--isSplitScreenAspectRatioForUnresizableAppsEnabled":
                     runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(pw);
                     break;
+                case "--isTranslucentLetterboxingEnabled":
+                    runSetTranslucentLetterboxingEnabled(pw);
+                    break;
                 default:
                     getErrPrintWriter().println(
                             "Error: Unrecognized letterbox style option: " + arg);
@@ -1096,6 +1122,9 @@
                         mLetterboxConfiguration
                                 .getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
                         break;
+                    case "isTranslucentLetterboxingEnabled":
+                        mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+                        break;
                     default:
                         getErrPrintWriter().println(
                                 "Error: Unrecognized letterbox style option: " + arg);
@@ -1196,6 +1225,7 @@
             mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
             mLetterboxConfiguration.resetIsEducationEnabled();
             mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+            mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
         }
     }
 
@@ -1232,7 +1262,6 @@
             pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
                     + mLetterboxConfiguration
                             .getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
-
             pw.println("Background type: "
                     + LetterboxConfiguration.letterboxBackgroundTypeToString(
                             mLetterboxConfiguration.getLetterboxBackgroundType()));
@@ -1242,6 +1271,12 @@
                     + mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadius());
             pw.println("    Wallpaper dark scrim alpha: "
                     + mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha());
+
+            if (mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
+                pw.println("Letterboxing for translucent activities: enabled");
+            } else {
+                pw.println("Letterboxing for translucent activities: disabled");
+            }
         }
         return 0;
     }
@@ -1434,12 +1469,16 @@
         pw.println("      --isSplitScreenAspectRatioForUnresizableAppsEnabled [true|1|false|0]");
         pw.println("        Whether using split screen aspect ratio as a default aspect ratio for");
         pw.println("        unresizable apps.");
+        pw.println("      --isTranslucentLetterboxingEnabled [true|1|false|0]");
+        pw.println("        Whether letterboxing for translucent activities is enabled.");
+
         pw.println("  reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
         pw.println("      |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
         pw.println("      |horizontalPositionMultiplier|verticalPositionMultiplier");
         pw.println("      |isHorizontalReachabilityEnabled|isVerticalReachabilityEnabled");
-        pw.println("      isEducationEnabled||defaultPositionMultiplierForHorizontalReachability");
-        pw.println("      ||defaultPositionMultiplierForVerticalReachability]");
+        pw.println("      |isEducationEnabled||defaultPositionMultiplierForHorizontalReachability");
+        pw.println("      |isTranslucentLetterboxingEnabled");
+        pw.println("      |defaultPositionMultiplierForVerticalReachability]");
         pw.println("    Resets overrides to default values for specified properties separated");
         pw.println("    by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'.");
         pw.println("    If no arguments provided, all values will be reset.");
diff --git a/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java b/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java
index a2f37a5..7dc8a31 100644
--- a/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java
+++ b/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java
@@ -22,11 +22,9 @@
 import static android.view.Surface.ROTATION_90;
 
 import android.annotation.Dimension;
-import android.annotation.Nullable;
 import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.graphics.RectF;
 import android.view.DisplayInfo;
+import android.view.Surface;
 import android.view.Surface.Rotation;
 
 public class CoordinateTransforms {
@@ -137,19 +135,24 @@
         out.postConcat(tmp);
     }
 
-    /**
-     * Transforms a rect using a transformation matrix
-     *
-     * @param transform the transformation to apply to the rect
-     * @param inOutRect the rect to transform
-     * @param tmp a temporary value, if null the function will allocate its own.
-     */
-    public static void transformRect(Matrix transform, Rect inOutRect, @Nullable RectF tmp) {
-        if (tmp == null) {
-            tmp = new RectF();
+    /** Computes the matrix that rotates the original w x h by the rotation delta. */
+    public static void computeRotationMatrix(int rotationDelta, int w, int h, Matrix outMatrix) {
+        switch (rotationDelta) {
+            case Surface.ROTATION_0:
+                outMatrix.reset();
+                break;
+            case Surface.ROTATION_90:
+                outMatrix.setRotate(90);
+                outMatrix.postTranslate(h, 0);
+                break;
+            case Surface.ROTATION_180:
+                outMatrix.setRotate(180);
+                outMatrix.postTranslate(w, h);
+                break;
+            case Surface.ROTATION_270:
+                outMatrix.setRotate(270);
+                outMatrix.postTranslate(0, w);
+                break;
         }
-        tmp.set(inOutRect);
-        transform.mapRect(tmp);
-        inOutRect.set((int) tmp.left, (int) tmp.top, (int) tmp.right, (int) tmp.bottom);
     }
 }
diff --git a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
deleted file mode 100644
index c11a6d0..0000000
--- a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
+++ /dev/null
@@ -1,55 +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.wm.utils;
-
-import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
-
-import android.graphics.Matrix;
-import android.hardware.HardwareBuffer;
-import android.view.Surface;
-
-
-/** Helper functions for the {@link com.android.server.wm.ScreenRotationAnimation} class*/
-public class RotationAnimationUtils {
-
-    /**
-     * @return whether the hardwareBuffer passed in is marked as protected.
-     */
-    public static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
-        return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
-    }
-
-    public static void createRotationMatrix(int rotation, int width, int height, Matrix outMatrix) {
-        switch (rotation) {
-            case Surface.ROTATION_0:
-                outMatrix.reset();
-                break;
-            case Surface.ROTATION_90:
-                outMatrix.setRotate(90, 0, 0);
-                outMatrix.postTranslate(height, 0);
-                break;
-            case Surface.ROTATION_180:
-                outMatrix.setRotate(180, 0, 0);
-                outMatrix.postTranslate(width, height);
-                break;
-            case Surface.ROTATION_270:
-                outMatrix.setRotate(270, 0, 0);
-                outMatrix.postTranslate(0, width);
-                break;
-        }
-    }
-}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 07819b9..e661688 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -71,7 +71,6 @@
         "com_android_server_PersistentDataBlockService.cpp",
         "com_android_server_am_LowMemDetector.cpp",
         "com_android_server_pm_PackageManagerShellCommandDataLoader.cpp",
-        "com_android_server_pm_Settings.cpp",
         "com_android_server_sensor_SensorService.cpp",
         "com_android_server_wm_TaskFpsCallbackController.cpp",
         "onload.cpp",
@@ -153,7 +152,6 @@
         "libpsi",
         "libdataloader",
         "libincfs",
-        "liblz4",
         "android.hardware.audio.common@2.0",
         "android.media.audio.common.types-V1-ndk",
         "android.hardware.broadcastradio@1.0",
@@ -234,26 +232,3 @@
         "com_android_server_app_GameManagerService.cpp",
     ],
 }
-
-// Settings JNI library for unit tests.
-cc_library_shared {
-    name: "libservices.core.settings.testonly",
-    defaults: ["libservices.core-libs"],
-
-    cpp_std: "c++2a",
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-Wno-unused-parameter",
-        "-Wthread-safety",
-    ],
-
-    srcs: [
-        "com_android_server_pm_Settings.cpp",
-        "onload_settings.cpp",
-    ],
-
-    header_libs: [
-        "bionic_libc_platform_headers",
-    ],
-}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 145e088..c36c571 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -120,6 +120,7 @@
     jmethodID getExcludedDeviceNames;
     jmethodID getInputPortAssociations;
     jmethodID getInputUniqueIdAssociations;
+    jmethodID getDeviceTypeAssociations;
     jmethodID getKeyRepeatTimeout;
     jmethodID getKeyRepeatDelay;
     jmethodID getHoverTapTimeout;
@@ -411,6 +412,8 @@
     void ensureSpriteControllerLocked();
     sp<SurfaceControl> getParentSurfaceForPointers(int displayId);
     static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName);
+    std::unordered_map<std::string, std::string> readMapFromInterleavedJavaArray(
+            jmethodID method, const char* methodName);
 
     static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); }
 };
@@ -583,21 +586,14 @@
         }
         env->DeleteLocalRef(portAssociations);
     }
-    outConfig->uniqueIdAssociations.clear();
-    jobjectArray uniqueIdAssociations = jobjectArray(
-            env->CallObjectMethod(mServiceObj, gServiceClassInfo.getInputUniqueIdAssociations));
-    if (!checkAndClearExceptionFromCallback(env, "getInputUniqueIdAssociations") &&
-        uniqueIdAssociations) {
-        jsize length = env->GetArrayLength(uniqueIdAssociations);
-        for (jsize i = 0; i < length / 2; i++) {
-            std::string inputDeviceUniqueId =
-                    getStringElementFromJavaArray(env, uniqueIdAssociations, 2 * i);
-            std::string displayUniqueId =
-                    getStringElementFromJavaArray(env, uniqueIdAssociations, 2 * i + 1);
-            outConfig->uniqueIdAssociations.insert({inputDeviceUniqueId, displayUniqueId});
-        }
-        env->DeleteLocalRef(uniqueIdAssociations);
-    }
+
+    outConfig->uniqueIdAssociations =
+            readMapFromInterleavedJavaArray(gServiceClassInfo.getInputUniqueIdAssociations,
+                                            "getInputUniqueIdAssociations");
+
+    outConfig->deviceTypeAssociations =
+            readMapFromInterleavedJavaArray(gServiceClassInfo.getDeviceTypeAssociations,
+                                            "getDeviceTypeAssociations");
 
     jint hoverTapTimeout = env->CallIntMethod(mServiceObj,
             gServiceClassInfo.getHoverTapTimeout);
@@ -647,6 +643,23 @@
     } // release lock
 }
 
+std::unordered_map<std::string, std::string> NativeInputManager::readMapFromInterleavedJavaArray(
+        jmethodID method, const char* methodName) {
+    JNIEnv* env = jniEnv();
+    jobjectArray javaArray = jobjectArray(env->CallObjectMethod(mServiceObj, method));
+    std::unordered_map<std::string, std::string> map;
+    if (!checkAndClearExceptionFromCallback(env, methodName) && javaArray) {
+        jsize length = env->GetArrayLength(javaArray);
+        for (jsize i = 0; i < length / 2; i++) {
+            std::string key = getStringElementFromJavaArray(env, javaArray, 2 * i);
+            std::string value = getStringElementFromJavaArray(env, javaArray, 2 * i + 1);
+            map.insert({key, value});
+        }
+    }
+    env->DeleteLocalRef(javaArray);
+    return map;
+}
+
 std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerController(
         int32_t /* deviceId */) {
     ATRACE_CALL();
@@ -2237,6 +2250,12 @@
             InputReaderConfiguration::CHANGE_DISPLAY_INFO);
 }
 
+static void nativeChangeTypeAssociation(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    im->getInputManager()->getReader().requestRefreshConfiguration(
+            InputReaderConfiguration::CHANGE_DEVICE_TYPE);
+}
+
 static void nativeSetMotionClassifierEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
@@ -2425,6 +2444,7 @@
         {"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay},
         {"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged},
         {"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation},
+        {"changeTypeAssociation", "()V", (void*)nativeChangeTypeAssociation},
         {"setDisplayEligibilityForPointerCapture", "(IZ)V",
          (void*)nativeSetDisplayEligibilityForPointerCapture},
         {"setMotionClassifierEnabled", "(Z)V", (void*)nativeSetMotionClassifierEnabled},
@@ -2546,6 +2566,9 @@
     GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociations, clazz,
                   "getInputUniqueIdAssociations", "()[Ljava/lang/String;");
 
+    GET_METHOD_ID(gServiceClassInfo.getDeviceTypeAssociations, clazz, "getDeviceTypeAssociations",
+                  "()[Ljava/lang/String;");
+
     GET_METHOD_ID(gServiceClassInfo.getKeyRepeatTimeout, clazz,
             "getKeyRepeatTimeout", "()I");
 
diff --git a/services/core/jni/com_android_server_pm_Settings.cpp b/services/core/jni/com_android_server_pm_Settings.cpp
deleted file mode 100644
index 9633a11..0000000
--- a/services/core/jni/com_android_server_pm_Settings.cpp
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define ATRACE_TAG ATRACE_TAG_ADB
-#define LOG_TAG "Settings-jni"
-#include <android-base/file.h>
-#include <android-base/logging.h>
-#include <android-base/no_destructor.h>
-#include <core_jni_helpers.h>
-#include <lz4frame.h>
-#include <nativehelper/JNIHelp.h>
-
-#include <vector>
-
-namespace android {
-
-namespace {
-
-struct LZ4FCContextDeleter {
-    void operator()(LZ4F_cctx* cctx) { LZ4F_freeCompressionContext(cctx); }
-};
-
-static constexpr int LZ4_BUFFER_SIZE = 64 * 1024;
-
-static bool writeToFile(std::vector<char>& outBuffer, int fdOut) {
-    if (!android::base::WriteFully(fdOut, outBuffer.data(), outBuffer.size())) {
-        PLOG(ERROR) << "Error to write to output file";
-        return false;
-    }
-    outBuffer.clear();
-    return true;
-}
-
-static bool compressAndWriteLz4(LZ4F_cctx* context, std::vector<char>& inBuffer,
-                                std::vector<char>& outBuffer, int fdOut) {
-    auto inSize = inBuffer.size();
-    if (inSize > 0) {
-        auto prvSize = outBuffer.size();
-        auto outSize = LZ4F_compressBound(inSize, nullptr);
-        outBuffer.resize(prvSize + outSize);
-        auto rc = LZ4F_compressUpdate(context, outBuffer.data() + prvSize, outSize, inBuffer.data(),
-                                      inSize, nullptr);
-        if (LZ4F_isError(rc)) {
-            LOG(ERROR) << "LZ4F_compressUpdate failed: " << LZ4F_getErrorName(rc);
-            return false;
-        }
-        outBuffer.resize(prvSize + rc);
-    }
-
-    if (outBuffer.size() > LZ4_BUFFER_SIZE) {
-        return writeToFile(outBuffer, fdOut);
-    }
-
-    return true;
-}
-
-static jboolean nativeCompressLz4(JNIEnv* env, jclass klass, jint fdIn, jint fdOut) {
-    LZ4F_cctx* cctx;
-    if (LZ4F_createCompressionContext(&cctx, LZ4F_VERSION) != 0) {
-        LOG(ERROR) << "Failed to initialize LZ4 compression context.";
-        return false;
-    }
-    std::unique_ptr<LZ4F_cctx, LZ4FCContextDeleter> context(cctx);
-
-    std::vector<char> inBuffer, outBuffer;
-    inBuffer.reserve(LZ4_BUFFER_SIZE);
-    outBuffer.reserve(2 * LZ4_BUFFER_SIZE);
-
-    LZ4F_preferences_t prefs;
-
-    memset(&prefs, 0, sizeof(prefs));
-
-    // Set compression parameters.
-    prefs.autoFlush = 0;
-    prefs.compressionLevel = 0;
-    prefs.frameInfo.blockMode = LZ4F_blockLinked;
-    prefs.frameInfo.blockSizeID = LZ4F_default;
-    prefs.frameInfo.blockChecksumFlag = LZ4F_noBlockChecksum;
-    prefs.frameInfo.contentChecksumFlag = LZ4F_contentChecksumEnabled;
-    prefs.favorDecSpeed = 0;
-
-    struct stat sb;
-    if (fstat(fdIn, &sb) == -1) {
-        PLOG(ERROR) << "Failed to obtain input file size.";
-        return false;
-    }
-    prefs.frameInfo.contentSize = sb.st_size;
-
-    // Write header first.
-    outBuffer.resize(LZ4F_HEADER_SIZE_MAX);
-    auto rc = LZ4F_compressBegin(context.get(), outBuffer.data(), outBuffer.size(), &prefs);
-    if (LZ4F_isError(rc)) {
-        LOG(ERROR) << "LZ4F_compressBegin failed: " << LZ4F_getErrorName(rc);
-        return false;
-    }
-    outBuffer.resize(rc);
-
-    bool eof = false;
-    while (!eof) {
-        constexpr auto capacity = LZ4_BUFFER_SIZE;
-        inBuffer.resize(capacity);
-        auto read = TEMP_FAILURE_RETRY(::read(fdIn, inBuffer.data(), inBuffer.size()));
-        if (read < 0) {
-            PLOG(ERROR) << "Failed to read from input file.";
-            return false;
-        }
-
-        inBuffer.resize(read);
-
-        if (read == 0) {
-            eof = true;
-        }
-
-        if (!compressAndWriteLz4(context.get(), inBuffer, outBuffer, fdOut)) {
-            return false;
-        }
-    }
-
-    // Footer.
-    auto prvSize = outBuffer.size();
-    outBuffer.resize(outBuffer.capacity());
-    rc = LZ4F_compressEnd(context.get(), outBuffer.data() + prvSize, outBuffer.size() - prvSize,
-                          nullptr);
-    if (LZ4F_isError(rc)) {
-        LOG(ERROR) << "LZ4F_compressEnd failed: " << LZ4F_getErrorName(rc);
-        return false;
-    }
-    outBuffer.resize(prvSize + rc);
-
-    if (!writeToFile(outBuffer, fdOut)) {
-        return false;
-    }
-
-    return true;
-}
-
-static const JNINativeMethod method_table[] = {
-        {"nativeCompressLz4", "(II)Z", (void*)nativeCompressLz4},
-};
-
-} // namespace
-
-int register_android_server_com_android_server_pm_Settings(JNIEnv* env) {
-    return jniRegisterNativeMethods(env, "com/android/server/pm/Settings", method_table,
-                                    NELEM(method_table));
-}
-
-} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 1845057..00f851f 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -56,7 +56,6 @@
 int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(JNIEnv* env);
 int register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(JNIEnv* env);
 int register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(JNIEnv* env);
-int register_android_server_com_android_server_pm_Settings(JNIEnv* env);
 int register_android_server_AdbDebuggingManager(JNIEnv* env);
 int register_android_server_FaceService(JNIEnv* env);
 int register_android_server_GpuService(JNIEnv* env);
@@ -115,7 +114,6 @@
     register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(env);
     register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(env);
     register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(env);
-    register_android_server_com_android_server_pm_Settings(env);
     register_android_server_AdbDebuggingManager(env);
     register_android_server_FaceService(env);
     register_android_server_GpuService(env);
diff --git a/services/core/jni/onload_settings.cpp b/services/core/jni/onload_settings.cpp
deleted file mode 100644
index b21c34a..0000000
--- a/services/core/jni/onload_settings.cpp
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "jni.h"
-#include "utils/Log.h"
-
-namespace android {
-int register_android_server_com_android_server_pm_Settings(JNIEnv* env);
-};
-
-using namespace android;
-
-extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
-    JNIEnv* env = NULL;
-    jint result = -1;
-
-    if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
-        ALOGE("GetEnv failed!");
-        return result;
-    }
-    ALOG_ASSERT(env, "Could not retrieve the env!");
-
-    register_android_server_com_android_server_pm_Settings(env);
-
-    return JNI_VERSION_1_4;
-}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
index 9af30ba..4634ff5 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
@@ -163,6 +163,7 @@
             "preferential_network_service_config";
     private static final String TAG_PROTECTED_PACKAGES = "protected_packages";
     private static final String TAG_SUSPENDED_PACKAGES = "suspended-packages";
+    private static final String TAG_MTE_POLICY = "mte-policy";
     private static final String ATTR_VALUE = "value";
     private static final String ATTR_LAST_NETWORK_LOGGING_NOTIFICATION = "last-notification";
     private static final String ATTR_NUM_NETWORK_LOGGING_NOTIFICATIONS = "num-notifications";
@@ -222,6 +223,8 @@
     int numNetworkLoggingNotifications = 0;
     long lastNetworkLoggingNotificationTimeMs = 0; // Time in milliseconds since epoch
 
+    @DevicePolicyManager.MtePolicy int mtePolicy = DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY;
+
     ActiveAdmin parentAdmin;
     final boolean isParent;
 
@@ -620,6 +623,9 @@
             }
             out.endTag(null, TAG_PREFERENTIAL_NETWORK_SERVICE_CONFIGS);
         }
+        if (mtePolicy != DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY) {
+            writeAttributeValueToXml(out, TAG_MTE_POLICY, mtePolicy);
+        }
     }
 
     private List<String> ssidsToStrings(Set<WifiSsid> ssids) {
@@ -906,6 +912,8 @@
                 if (!configs.isEmpty()) {
                     mPreferentialNetworkServiceConfigs = configs;
                 }
+            } else if (TAG_MTE_POLICY.equals(tag)) {
+                mtePolicy = parser.getAttributeInt(null, ATTR_VALUE);
             } else {
                 Slogf.w(LOG_TAG, "Unknown admin tag: %s", tag);
                 XmlUtils.skipCurrentTag(parser);
@@ -1338,5 +1346,8 @@
             }
             pw.decreaseIndent();
         }
+
+        pw.print("mtePolicy=");
+        pw.println(mtePolicy);
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e93809d..c42ddf8 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -5387,7 +5387,8 @@
                         }
                         if (!mInjector.storageManagerIsFileBasedEncryptionEnabled()) {
                             throw new UnsupportedOperationException(
-                                    "FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY only applies to FBE devices");
+                                    "FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY only applies to FBE"
+                                        + " devices");
                         }
                         mUserManager.evictCredentialEncryptionKey(callingUserId);
                     }
@@ -19240,4 +19241,55 @@
                 KEEP_PROFILES_RUNNING_FLAG,
                 DEFAULT_KEEP_PROFILES_RUNNING_FLAG);
     }
+
+    @Override
+    public void setMtePolicy(int flags) {
+        final Set<Integer> allowedModes =
+                Set.of(
+                        DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY,
+                        DevicePolicyManager.MTE_DISABLED,
+                        DevicePolicyManager.MTE_ENABLED);
+        Preconditions.checkArgument(
+                allowedModes.contains(flags), "Provided mode is not one of the allowed values.");
+        final CallerIdentity caller = getCallerIdentity();
+        if (flags == DevicePolicyManager.MTE_DISABLED) {
+            Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller));
+        } else {
+            Preconditions.checkCallAuthorization(
+                    isDefaultDeviceOwner(caller)
+                            || isProfileOwnerOfOrganizationOwnedDevice(caller));
+        }
+        synchronized (getLockObject()) {
+            ActiveAdmin admin =
+                    getDeviceOwnerOrProfileOwnerOfOrganizationOwnedDeviceLocked(
+                            UserHandle.USER_SYSTEM);
+            if (admin != null) {
+                final String memtagProperty = "arm64.memtag.bootctl";
+                if (flags == DevicePolicyManager.MTE_ENABLED) {
+                    mInjector.systemPropertiesSet(memtagProperty, "memtag");
+                } else if (flags == DevicePolicyManager.MTE_DISABLED) {
+                    mInjector.systemPropertiesSet(memtagProperty, "memtag-off");
+                }
+                admin.mtePolicy = flags;
+                saveSettingsLocked(caller.getUserId());
+            }
+        }
+    }
+
+    @Override
+    public int getMtePolicy() {
+        final CallerIdentity caller = getCallerIdentity();
+        Preconditions.checkCallAuthorization(
+                isDefaultDeviceOwner(caller)
+                        || isProfileOwnerOfOrganizationOwnedDevice(caller)
+                        || isSystemUid(caller));
+        synchronized (getLockObject()) {
+            ActiveAdmin admin =
+                    getDeviceOwnerOrProfileOwnerOfOrganizationOwnedDeviceLocked(
+                            UserHandle.USER_SYSTEM);
+            return admin != null
+                    ? admin.mtePolicy
+                    : DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY;
+        }
+    }
 }
diff --git a/services/java/com/android/server/BootUserInitializer.java b/services/java/com/android/server/BootUserInitializer.java
index c3329795..deebfc7 100644
--- a/services/java/com/android/server/BootUserInitializer.java
+++ b/services/java/com/android/server/BootUserInitializer.java
@@ -102,6 +102,7 @@
         switchToInitialUser(initialUserId);
     }
 
+    /* TODO(b/261791491): STOPSHIP - SUW should be responsible for this. */
     private void provisionHeadlessSystemUser() {
         if (isDeviceProvisioned()) {
             Slogf.d(TAG, "provisionHeadlessSystemUser(): already provisioned");
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 509d75b..c68eea8 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -109,6 +109,7 @@
 import com.android.server.appbinding.AppBindingService;
 import com.android.server.art.ArtManagerLocal;
 import com.android.server.art.ArtModuleServiceInitializer;
+import com.android.server.art.DexUseManagerLocal;
 import com.android.server.attention.AttentionManagerService;
 import com.android.server.audio.AudioService;
 import com.android.server.biometrics.AuthService;
@@ -1226,6 +1227,13 @@
             Watchdog.getInstance().resumeWatchingCurrentThread("packagemanagermain");
         }
 
+        // DexUseManagerLocal needs to be loaded after PackageManagerLocal has been registered, but
+        // before PackageManagerService starts processing binder calls to notifyDexLoad.
+        // DexUseManagerLocal may also call artd, so ensure ArtModuleServiceManager is instantiated.
+        ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager());
+        LocalManagerRegistry.addManager(
+                DexUseManagerLocal.class, DexUseManagerLocal.createInstance());
+
         mFirstBoot = mPackageManagerService.isFirstBoot();
         mPackageManager = mSystemContext.getPackageManager();
         t.traceEnd();
@@ -2729,7 +2737,6 @@
         t.traceEnd();
 
         t.traceBegin("ArtManagerLocal");
-        ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager());
         LocalManagerRegistry.addManager(ArtManagerLocal.class, new ArtManagerLocal(context));
         t.traceEnd();
 
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 73c2ccc..f493b89 100644
--- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
@@ -20,15 +20,15 @@
 import com.android.internal.annotations.Keep
 import com.android.server.LocalManagerRegistry
 import com.android.server.LocalServices
+import com.android.server.SystemConfig
 import com.android.server.SystemService
 import com.android.server.appop.AppOpsCheckingServiceInterface
 import com.android.server.permission.access.appop.AppOpService
-import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
+import com.android.server.permission.access.collection.IntSet
 import com.android.server.permission.access.permission.PermissionService
 import com.android.server.pm.PackageManagerLocal
 import com.android.server.pm.UserManagerService
 import com.android.server.pm.permission.PermissionManagerServiceInterface
-import com.android.server.pm.permission.PermissionManagerServiceInternal
 import com.android.server.pm.pkg.PackageState
 
 @Keep
@@ -46,6 +46,7 @@
 
     private lateinit var packageManagerLocal: PackageManagerLocal
     private lateinit var userManagerService: UserManagerService
+    private lateinit var systemConfig: SystemConfig
 
     override fun onStart() {
         appOpService = AppOpService(this)
@@ -59,12 +60,16 @@
         packageManagerLocal =
             LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal::class.java)
         userManagerService = UserManagerService.getInstance()
+        systemConfig = SystemConfig.getInstance()
 
         val userIds = IntSet(userManagerService.userIdsIncludingPreCreated)
-        val packageStates = packageManagerLocal.packageStates
+        val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
+        val permissionAllowlist = systemConfig.permissionAllowlist
 
         val state = AccessState()
-        policy.initialize(state, userIds, packageStates)
+        policy.initialize(
+            state, userIds, packageStates, disabledSystemPackageStates, permissionAllowlist
+        )
         persistence.read(state)
         this.state = state
 
@@ -96,51 +101,60 @@
     }
 
     internal fun onStorageVolumeMounted(volumeUuid: String?, isSystemUpdated: Boolean) {
-        val packageStates = packageManagerLocal.packageStates
+        val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
         mutateState {
-            with(policy) { onStorageVolumeMounted(packageStates, volumeUuid, isSystemUpdated) }
+            with(policy) {
+                onStorageVolumeMounted(
+                    packageStates, disabledSystemPackageStates, volumeUuid, isSystemUpdated
+                )
+            }
         }
     }
 
     internal fun onPackageAdded(packageName: String) {
-        val packageStates = packageManagerLocal.packageStates
+        val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
         mutateState {
-            with(policy) { onPackageAdded(packageStates, packageName) }
+            with(policy) { onPackageAdded(packageStates, disabledSystemPackageStates, packageName) }
         }
     }
 
     internal fun onPackageRemoved(packageName: String, appId: Int) {
-        val packageStates = packageManagerLocal.packageStates
+        val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
         mutateState {
-            with(policy) { onPackageRemoved(packageStates, packageName, appId) }
+            with(policy) {
+                onPackageRemoved(packageStates, disabledSystemPackageStates, packageName, appId)
+            }
         }
     }
 
-    internal fun onPackageInstalled(
-        packageName: String,
-        params: PermissionManagerServiceInternal.PackageInstalledParams,
-        userId: Int
-    ) {
-        val packageStates = packageManagerLocal.packageStates
+    internal fun onPackageInstalled(packageName: String, userId: Int) {
+        val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
         mutateState {
-            with(policy) { onPackageInstalled(packageStates, packageName, params, userId) }
+            with(policy) {
+                onPackageInstalled(packageStates, disabledSystemPackageStates, packageName, userId)
+            }
         }
     }
 
     internal fun onPackageUninstalled(packageName: String, appId: Int, userId: Int) {
-        val packageStates = packageManagerLocal.packageStates
+        val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
         mutateState {
-            with(policy) { onPackageUninstalled(packageStates, packageName, appId, userId) }
+            with(policy) {
+                onPackageUninstalled(
+                    packageStates, disabledSystemPackageStates, packageName, appId, userId
+                )
+            }
         }
     }
 
-    private val PackageManagerLocal.packageStates: Map<String, PackageState>
-        get() = withUnfilteredSnapshot().use { it.packageStates }
+    private val PackageManagerLocal.allPackageStates:
+        Pair<Map<String, PackageState>, Map<String, PackageState>>
+        get() = withUnfilteredSnapshot().use { it.packageStates to it.disabledSystemPackageStates }
 
     internal inline fun <T> getState(action: GetStateScope.() -> T): T =
         GetStateScope(state).action()
 
-    internal inline fun mutateState(action: MutateStateScope.() -> Unit) {
+    internal inline fun mutateState(crossinline action: MutateStateScope.() -> Unit) {
         synchronized(stateLock) {
             val oldState = state
             val newState = oldState.copy()
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 0201dd0..89316c2 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
@@ -26,7 +26,7 @@
 import com.android.server.permission.access.util.forEachTag
 import com.android.server.permission.access.util.tag
 import com.android.server.permission.access.util.tagName
-import com.android.server.pm.permission.PermissionManagerServiceInternal
+import com.android.server.pm.permission.PermissionAllowlist
 import com.android.server.pm.pkg.PackageState
 
 class AccessPolicy private constructor(
@@ -54,14 +54,22 @@
         with(getSchemePolicy(subject, `object`)) { setDecision(subject, `object`, decision) }
     }
 
-    fun initialize(state: AccessState, userIds: IntSet, packageStates: Map<String, PackageState>) {
+    fun initialize(
+        state: AccessState,
+        userIds: IntSet,
+        packageStates: Map<String, PackageState>,
+        disabledSystemPackageStates: Map<String, PackageState>,
+        permissionAllowlist: PermissionAllowlist
+    ) {
         state.systemState.apply {
             this.userIds += userIds
             this.packageStates = packageStates
+            this.disabledSystemPackageStates = disabledSystemPackageStates
             packageStates.forEach { (_, packageState) ->
                 appIds.getOrPut(packageState.appId) { IndexedListSet() }
                     .add(packageState.packageName)
             }
+            this.permissionAllowlist = permissionAllowlist
         }
     }
 
@@ -83,10 +91,14 @@
 
     fun MutateStateScope.onStorageVolumeMounted(
         packageStates: Map<String, PackageState>,
+        disabledSystemPackageStates: Map<String, PackageState>,
         volumeUuid: String?,
         isSystemUpdated: Boolean
     ) {
-        newState.systemState.packageStates = packageStates
+        newState.systemState.apply {
+            this.packageStates = packageStates
+            this.disabledSystemPackageStates = disabledSystemPackageStates
+        }
         forEachSchemePolicy {
             with(it) { onStorageVolumeMounted(volumeUuid, isSystemUpdated) }
         }
@@ -94,18 +106,24 @@
 
     fun MutateStateScope.onPackageAdded(
         packageStates: Map<String, PackageState>,
+        disabledSystemPackageStates: Map<String, PackageState>,
         packageName: String
     ) {
-        newState.systemState.packageStates = packageStates
-        var isAppIdAdded = false
         val packageState = packageStates[packageName]
-        // TODO(zhanghai): Remove check before submission.
-        checkNotNull(packageState)
+        // TODO(zhanghai): STOPSHIP: Remove check before feature enable.
+        checkNotNull(packageState) {
+            "Added package $packageName isn't found in packageStates in onPackageAdded()"
+        }
         val appId = packageState.appId
-        newState.systemState.appIds.getOrPut(appId) {
-            isAppIdAdded = true
-            IndexedListSet()
-        }.add(packageName)
+        var isAppIdAdded = false
+        newState.systemState.apply {
+            this.packageStates = packageStates
+            this.disabledSystemPackageStates = disabledSystemPackageStates
+            appIds.getOrPut(appId) {
+                isAppIdAdded = true
+                IndexedListSet()
+            }.add(packageName)
+        }
         if (isAppIdAdded) {
             forEachSchemePolicy {
                 with(it) { onAppIdAdded(appId) }
@@ -118,18 +136,22 @@
 
     fun MutateStateScope.onPackageRemoved(
         packageStates: Map<String, PackageState>,
+        disabledSystemPackageStates: Map<String, PackageState>,
         packageName: String,
         appId: Int
     ) {
-        newState.systemState.packageStates = packageStates
+        // TODO(zhanghai): STOPSHIP: Remove check before feature enable.
+        check(packageName !in packageStates) {
+            "Removed package $packageName is still in packageStates in onPackageRemoved()"
+        }
         var isAppIdRemoved = false
-        // TODO(zhanghai): Remove check before submission.
-        check(packageName !in packageStates)
-        newState.systemState.appIds.apply appIds@{
-            this[appId]?.apply {
+        newState.systemState.apply {
+            this.packageStates = packageStates
+            this.disabledSystemPackageStates = disabledSystemPackageStates
+            appIds[appId]?.apply {
                 this -= packageName
                 if (isEmpty()) {
-                    this@appIds -= appId
+                    appIds -= appId
                     isAppIdRemoved = true
                 }
             }
@@ -146,26 +168,35 @@
 
     fun MutateStateScope.onPackageInstalled(
         packageStates: Map<String, PackageState>,
+        disabledSystemPackageStates: Map<String, PackageState>,
         packageName: String,
-        params: PermissionManagerServiceInternal.PackageInstalledParams,
         userId: Int
     ) {
-        newState.systemState.packageStates = packageStates
+        newState.systemState.apply {
+            this.packageStates = packageStates
+            this.disabledSystemPackageStates = disabledSystemPackageStates
+        }
         val packageState = packageStates[packageName]
-        // TODO(zhanghai): Remove check before submission.
-        checkNotNull(packageState)
+        // TODO(zhanghai): STOPSHIP: Remove check before feature enable.
+        checkNotNull(packageState) {
+            "Installed package $packageName isn't found in packageStates in onPackageInstalled()"
+        }
         forEachSchemePolicy {
-            with(it) { onPackageInstalled(packageState, params, userId) }
+            with(it) { onPackageInstalled(packageState, userId) }
         }
     }
 
     fun MutateStateScope.onPackageUninstalled(
         packageStates: Map<String, PackageState>,
+        disabledSystemPackageStates: Map<String, PackageState>,
         packageName: String,
         appId: Int,
         userId: Int
     ) {
-        newState.systemState.packageStates = packageStates
+        newState.systemState.apply {
+            this.packageStates = packageStates
+            this.disabledSystemPackageStates = disabledSystemPackageStates
+        }
         forEachSchemePolicy {
             with(it) { onPackageUninstalled(packageName, appId, userId) }
         }
@@ -241,10 +272,6 @@
 }
 
 abstract class SchemePolicy {
-    @Volatile
-    private var onDecisionChangedListeners = IndexedListSet<OnDecisionChangedListener>()
-    private val onDecisionChangedListenersLock = Any()
-
     abstract val subjectScheme: String
 
     abstract val objectScheme: String
@@ -257,30 +284,6 @@
         decision: Int
     )
 
-    fun addOnDecisionChangedListener(listener: OnDecisionChangedListener) {
-        synchronized(onDecisionChangedListenersLock) {
-            onDecisionChangedListeners = onDecisionChangedListeners + listener
-        }
-    }
-
-    fun removeOnDecisionChangedListener(listener: OnDecisionChangedListener) {
-        synchronized(onDecisionChangedListenersLock) {
-            onDecisionChangedListeners = onDecisionChangedListeners - listener
-        }
-    }
-
-    protected fun notifyOnDecisionChangedListeners(
-        subject: AccessUri,
-        `object`: AccessUri,
-        oldDecision: Int,
-        newDecision: Int
-    ) {
-        val listeners = onDecisionChangedListeners
-        listeners.forEachIndexed { _, it ->
-            it.onDecisionChanged(subject, `object`, oldDecision, newDecision)
-        }
-    }
-
     open fun MutateStateScope.onUserAdded(userId: Int) {}
 
     open fun MutateStateScope.onUserRemoved(userId: Int) {}
@@ -298,11 +301,7 @@
 
     open fun MutateStateScope.onPackageRemoved(packageName: String, appId: Int) {}
 
-    open fun MutateStateScope.onPackageInstalled(
-        packageState: PackageState,
-        params: PermissionManagerServiceInternal.PackageInstalledParams,
-        userId: Int
-    ) {}
+    open fun MutateStateScope.onPackageInstalled(packageState: PackageState, userId: Int) {}
 
     open fun MutateStateScope.onPackageUninstalled(packageName: String, appId: Int, userId: Int) {}
 
@@ -313,13 +312,4 @@
     open fun BinaryXmlPullParser.parseUserState(userId: Int, userState: UserState) {}
 
     open fun BinaryXmlSerializer.serializeUserState(userId: Int, userState: UserState) {}
-
-    fun interface OnDecisionChangedListener {
-        fun onDecisionChanged(
-            subject: AccessUri,
-            `object`: AccessUri,
-            oldDecision: Int,
-            newDecision: Int
-        )
-    }
 }
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 4a2c78a..fae1ec3 100644
--- a/services/permission/java/com/android/server/permission/access/AccessState.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessState.kt
@@ -19,15 +19,22 @@
 import android.content.pm.PermissionGroupInfo
 import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
 import com.android.server.permission.access.permission.Permission
+import com.android.server.pm.permission.PermissionAllowlist
 import com.android.server.pm.pkg.PackageState
 
 class AccessState private constructor(
     val systemState: SystemState,
     val userStates: IntMap<UserState>
 ) {
-    constructor() : this(SystemState(), IntMap())
+    constructor() : this(
+        SystemState(),
+        IntMap()
+    )
 
-    fun copy(): AccessState = AccessState(systemState.copy(), userStates.copy { it.copy() })
+    fun copy(): AccessState = AccessState(
+        systemState.copy(),
+        userStates.copy { it.copy() }
+    )
 }
 
 class SystemState private constructor(
@@ -39,30 +46,26 @@
     val knownPackages: IntMap<IndexedListSet<String>>,
     // A map of userId to packageName
     val deviceAndProfileOwners: IntMap<String>,
-    // A map of packageName to (A map of oem permission name to whether it's granted)
-    val oemPermissions: IndexedMap<String, IndexedMap<String, Boolean>>,
     val privilegedPermissionAllowlistSourcePackageNames: IndexedListSet<String>,
-    // A map of packageName to a set of vendor priv app permission names
-    val vendorPrivAppPermissions: Map<String, Set<String>>,
-    val productPrivAppPermissions: Map<String, Set<String>>,
-    val systemExtPrivAppPermissions: Map<String, Set<String>>,
-    val privAppPermissions: Map<String, Set<String>>,
-    val apexPrivAppPermissions: Map<String, Map<String, Set<String>>>,
-    val vendorPrivAppDenyPermissions: Map<String, Set<String>>,
-    val productPrivAppDenyPermissions: Map<String, Set<String>>,
-    val systemExtPrivAppDenyPermissions: Map<String, Set<String>>,
-    val apexPrivAppDenyPermissions: Map<String, Map<String, Set<String>>>,
-    val privAppDenyPermissions: Map<String, Set<String>>,
+    var permissionAllowlist: PermissionAllowlist,
     val implicitToSourcePermissions: Map<String, Set<String>>,
     val permissionGroups: IndexedMap<String, PermissionGroupInfo>,
     val permissionTrees: IndexedMap<String, Permission>,
     val permissions: IndexedMap<String, Permission>
 ) : WritableState() {
     constructor() : this(
-        IntSet(), emptyMap(), emptyMap(), IntMap(), IntMap(), IntMap(), IndexedMap(),
-        IndexedListSet(), IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(),
-        IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(),
-        IndexedMap(), IndexedMap(), IndexedMap()
+        IntSet(),
+        emptyMap(),
+        emptyMap(),
+        IntMap(),
+        IntMap(),
+        IntMap(),
+        IndexedListSet(),
+        PermissionAllowlist(),
+        IndexedMap(),
+        IndexedMap(),
+        IndexedMap(),
+        IndexedMap()
     )
 
     fun copy(): SystemState =
@@ -73,18 +76,8 @@
             appIds.copy { it.copy() },
             knownPackages.copy { it.copy() },
             deviceAndProfileOwners.copy { it },
-            oemPermissions.copy { it.copy { it } },
             privilegedPermissionAllowlistSourcePackageNames.copy(),
-            vendorPrivAppPermissions,
-            productPrivAppPermissions,
-            systemExtPrivAppPermissions,
-            privAppPermissions,
-            apexPrivAppPermissions,
-            vendorPrivAppDenyPermissions,
-            productPrivAppDenyPermissions,
-            systemExtPrivAppDenyPermissions,
-            apexPrivAppDenyPermissions,
-            privAppDenyPermissions,
+            permissionAllowlist,
             implicitToSourcePermissions,
             permissionGroups.copy { it },
             permissionTrees.copy { it },
@@ -98,7 +91,11 @@
     val uidAppOpModes: IntMap<IndexedMap<String, Int>>,
     val packageAppOpModes: IndexedMap<String, IndexedMap<String, Int>>
 ) : WritableState() {
-    constructor() : this(IntMap(), IntMap(), IndexedMap())
+    constructor() : this(
+        IntMap(),
+        IntMap(),
+        IndexedMap()
+    )
 
     fun copy(): UserState = UserState(
         uidPermissionFlags.copy { it.copy { it } },
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 a1a5e2d..7f4e0f7 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
@@ -16,50 +16,17 @@
 
 package com.android.server.permission.access.appop
 
-import android.app.AppOpsManager
 import com.android.modules.utils.BinaryXmlPullParser
 import com.android.modules.utils.BinaryXmlSerializer
-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 GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int {
-        `object` as AppOpUri
-        return getModes(subject)
-            .getWithDefault(`object`.appOpName, opToDefaultMode(`object`.appOpName))
-    }
-
-    override fun MutateStateScope.setDecision(
-        subject: AccessUri,
-        `object`: AccessUri,
-        decision: Int
-    ) {
-        `object` as AppOpUri
-        val modes = getOrCreateModes(subject)
-        val oldMode = modes.putWithDefault(`object`.appOpName, decision,
-            opToDefaultMode(`object`.appOpName))
-        if (modes.isEmpty()) {
-            removeModes(subject)
-        }
-        if (oldMode != decision) {
-            notifyOnDecisionChangedListeners(subject, `object`, oldMode, decision)
-        }
-    }
-
-    abstract fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>?
-
-    abstract fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int>
-
-    abstract fun MutateStateScope.removeModes(subject: AccessUri)
-
-    // TODO need to check that [AppOpsManager.getSystemAlertWindowDefault] works; likely no issue
-    //  since running in system process.
-    private fun opToDefaultMode(appOpName: String) = AppOpsManager.opToDefaultMode(appOpName)
+abstract class BaseAppOpPolicy(
+    private val persistence: BaseAppOpPersistence
+) : SchemePolicy() {
+    override val objectScheme: String
+        get() = AppOpUri.SCHEME
 
     override fun BinaryXmlPullParser.parseUserState(userId: Int, userState: UserState) {
         with(persistence) { this@parseUserState.parseUserState(userId, userState) }
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 f4d6bfd..607e512 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,40 +16,101 @@
 
 package com.android.server.permission.access.appop
 
+import android.app.AppOpsManager
 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
 
 class PackageAppOpPolicy : BaseAppOpPolicy(PackageAppOpPersistence()) {
+    @Volatile
+    private var onAppOpModeChangedListeners = IndexedListSet<OnAppOpModeChangedListener>()
+    private val onAppOpModeChangedListenersLock = Any()
+
     override val subjectScheme: String
         get() = PackageUri.SCHEME
 
-    override val objectScheme: String
-        get() = AppOpUri.SCHEME
-
-    override fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>? {
+    override fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int {
         subject as PackageUri
-        return state.userStates[subject.userId]?.packageAppOpModes?.get(subject.packageName)
+        `object` as AppOpUri
+        return getAppOpMode(subject.packageName, subject.userId, `object`.appOpName)
     }
 
-    override fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int> {
+    override fun MutateStateScope.setDecision(
+        subject: AccessUri,
+        `object`: AccessUri,
+        decision: Int
+    ) {
         subject as PackageUri
-        return newState.userStates.getOrPut(subject.userId) { UserState() }
-            .packageAppOpModes.getOrPut(subject.packageName) { IndexedMap() }
-    }
-
-    override fun MutateStateScope.removeModes(subject: AccessUri) {
-        subject as PackageUri
-        newState.userStates[subject.userId]?.packageAppOpModes?.remove(subject.packageName)
+        `object` as AppOpUri
+        setAppOpMode(subject.packageName, subject.userId, `object`.appOpName, decision)
     }
 
     override fun MutateStateScope.onPackageRemoved(packageName: String, appId: Int) {
         newState.userStates.forEachIndexed { _, _, userState ->
             userState.packageAppOpModes -= packageName
+            userState.requestWrite()
+            // Skip notifying the change listeners since the package no longer exists.
         }
     }
+
+    fun MutateStateScope.removeAppOpModes(packageName: String, userId: Int): Boolean =
+        newState.userStates[userId].packageAppOpModes.remove(packageName) != null
+
+    fun GetStateScope.getAppOpMode(packageName: String, userId: Int, appOpName: String): Int =
+        state.userStates[userId].packageAppOpModes[packageName]
+            .getWithDefault(appOpName, AppOpsManager.opToDefaultMode(appOpName))
+
+    fun MutateStateScope.setAppOpMode(
+        packageName: String,
+        userId: Int,
+        appOpName: String,
+        mode: Int
+    ): Boolean {
+        val userState = newState.userStates[userId]
+        val packageAppOpModes = userState.packageAppOpModes
+        var appOpModes = packageAppOpModes[packageName]
+        val defaultMode = AppOpsManager.opToDefaultMode(appOpName)
+        val oldMode = appOpModes.getWithDefault(appOpName, defaultMode)
+        if (oldMode == mode) {
+            return false
+        }
+        if (appOpModes == null) {
+            appOpModes = IndexedMap()
+            packageAppOpModes[packageName] = appOpModes
+        }
+        appOpModes.putWithDefault(appOpName, mode, defaultMode)
+        if (appOpModes.isEmpty()) {
+            packageAppOpModes -= packageName
+        }
+        userState.requestWrite()
+        onAppOpModeChangedListeners.forEachIndexed { _, it ->
+            it.onAppOpModeChanged(packageName, userId, appOpName, oldMode, mode)
+        }
+        return true
+    }
+
+    fun addOnAppOpModeChangedListener(listener: OnAppOpModeChangedListener) {
+        synchronized(onAppOpModeChangedListenersLock) {
+            onAppOpModeChangedListeners = onAppOpModeChangedListeners + listener
+        }
+    }
+
+    fun removeOnAppOpModeChangedListener(listener: OnAppOpModeChangedListener) {
+        synchronized(onAppOpModeChangedListenersLock) {
+            onAppOpModeChangedListeners = onAppOpModeChangedListeners - listener
+        }
+    }
+
+    fun interface OnAppOpModeChangedListener {
+        fun onAppOpModeChanged(
+            packageName: String,
+            userId: Int,
+            appOpName: String,
+            oldMode: Int,
+            newMode: Int
+        )
+    }
 }
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 862db8f..0b01038 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,40 +16,104 @@
 
 package com.android.server.permission.access.appop
 
+import android.app.AppOpsManager
 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
 
 class UidAppOpPolicy : BaseAppOpPolicy(UidAppOpPersistence()) {
+    @Volatile
+    private var onAppOpModeChangedListeners = IndexedListSet<OnAppOpModeChangedListener>()
+    private val onAppOpModeChangedListenersLock = Any()
+
     override val subjectScheme: String
         get() = UidUri.SCHEME
 
-    override val objectScheme: String
-        get() = AppOpUri.SCHEME
-
-    override fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>? {
+    override fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int {
         subject as UidUri
-        return state.userStates[subject.userId]?.uidAppOpModes?.get(subject.appId)
+        `object` as AppOpUri
+        return getAppOpMode(subject.appId, subject.userId, `object`.appOpName)
     }
 
-    override fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int> {
+    override fun MutateStateScope.setDecision(
+        subject: AccessUri,
+        `object`: AccessUri,
+        decision: Int
+    ) {
         subject as UidUri
-        return newState.userStates.getOrPut(subject.userId) { UserState() }
-            .uidAppOpModes.getOrPut(subject.appId) { IndexedMap() }
-    }
-
-    override fun MutateStateScope.removeModes(subject: AccessUri) {
-        subject as UidUri
-        newState.userStates[subject.userId]?.uidAppOpModes?.remove(subject.appId)
+        `object` as AppOpUri
+        setAppOpMode(subject.appId, subject.userId, `object`.appOpName, decision)
     }
 
     override fun MutateStateScope.onAppIdRemoved(appId: Int) {
         newState.userStates.forEachIndexed { _, _, userState ->
             userState.uidAppOpModes -= appId
+            userState.requestWrite()
+            // Skip notifying the change listeners since the app ID no longer exists.
         }
     }
+
+    fun GetStateScope.getAppOpModes(appId: Int, userId: Int): IndexedMap<String, Int>? =
+        state.userStates[userId].uidAppOpModes[appId]
+
+    fun MutateStateScope.removeAppOpModes(appId: Int, userId: Int): Boolean =
+        newState.userStates[userId].uidAppOpModes.removeReturnOld(appId) != null
+
+    fun GetStateScope.getAppOpMode(appId: Int, userId: Int, appOpName: String): Int =
+        state.userStates[userId].uidAppOpModes[appId]
+            .getWithDefault(appOpName, AppOpsManager.opToDefaultMode(appOpName))
+
+    fun MutateStateScope.setAppOpMode(
+        appId: Int,
+        userId: Int,
+        appOpName: String,
+        mode: Int
+    ): Boolean {
+        val userState = newState.userStates[userId]
+        val uidAppOpModes = userState.uidAppOpModes
+        var appOpModes = uidAppOpModes[appId]
+        val defaultMode = AppOpsManager.opToDefaultMode(appOpName)
+        val oldMode = appOpModes.getWithDefault(appOpName, defaultMode)
+        if (oldMode == mode) {
+            return false
+        }
+        if (appOpModes == null) {
+            appOpModes = IndexedMap()
+            uidAppOpModes[appId] = appOpModes
+        }
+        appOpModes.putWithDefault(appOpName, mode, defaultMode)
+        if (appOpModes.isEmpty()) {
+            uidAppOpModes -= appId
+        }
+        userState.requestWrite()
+        onAppOpModeChangedListeners.forEachIndexed { _, it ->
+            it.onAppOpModeChanged(appId, userId, appOpName, oldMode, mode)
+        }
+        return true
+    }
+
+    fun addOnAppOpModeChangedListener(listener: OnAppOpModeChangedListener) {
+        synchronized(onAppOpModeChangedListenersLock) {
+            onAppOpModeChangedListeners = onAppOpModeChangedListeners + listener
+        }
+    }
+
+    fun removeOnAppOpModeChangedListener(listener: OnAppOpModeChangedListener) {
+        synchronized(onAppOpModeChangedListenersLock) {
+            onAppOpModeChangedListeners = onAppOpModeChangedListeners - listener
+        }
+    }
+
+    fun interface OnAppOpModeChangedListener {
+        fun onAppOpModeChanged(
+            appId: Int,
+            userId: Int,
+            appOpName: String,
+            oldMode: Int,
+            newMode: Int
+        )
+    }
 }
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
index 9cb2e86..c4d07fe 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
@@ -19,8 +19,8 @@
 typealias IndexedList<T> = ArrayList<T>
 
 inline fun <T> IndexedList<T>.allIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (!predicate(index, this[index])) {
+    forEachIndexed { index, element ->
+        if (!predicate(index, element)) {
             return false
         }
     }
@@ -28,8 +28,8 @@
 }
 
 inline fun <T> IndexedList<T>.anyIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, this[index])) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return true
         }
     }
@@ -45,6 +45,12 @@
     }
 }
 
+inline fun <T> IndexedList<T>.forEachReversedIndexed(action: (Int, T) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, this[index])
+    }
+}
+
 @Suppress("NOTHING_TO_INLINE")
 inline operator fun <T> IndexedList<T>.minus(element: T): IndexedList<T> =
     copy().apply { this -= element }
@@ -55,8 +61,8 @@
 }
 
 inline fun <T> IndexedList<T>.noneIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, this[index])) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return false
         }
     }
@@ -74,8 +80,8 @@
 
 inline fun <T> IndexedList<T>.removeAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (predicate(index, this[index])) {
+    forEachReversedIndexed { index, element ->
+        if (predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
@@ -85,8 +91,8 @@
 
 inline fun <T> IndexedList<T>.retainAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (!predicate(index, this[index])) {
+    forEachReversedIndexed { index, element ->
+        if (!predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedListSet.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedListSet.kt
index 1c42c50..c40f7ee 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedListSet.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedListSet.kt
@@ -70,8 +70,8 @@
 }
 
 inline fun <T> IndexedListSet<T>.allIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (!predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (!predicate(index, element)) {
             return false
         }
     }
@@ -79,8 +79,8 @@
 }
 
 inline fun <T> IndexedListSet<T>.anyIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return true
         }
     }
@@ -93,6 +93,12 @@
     }
 }
 
+inline fun <T> IndexedListSet<T>.forEachReversedIndexed(action: (Int, T) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, elementAt(index))
+    }
+}
+
 inline val <T> IndexedListSet<T>.lastIndex: Int
     get() = size - 1
 
@@ -106,8 +112,8 @@
 }
 
 inline fun <T> IndexedListSet<T>.noneIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return false
         }
     }
@@ -125,8 +131,8 @@
 
 inline fun <T> IndexedListSet<T>.removeAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (predicate(index, elementAt(index))) {
+    forEachReversedIndexed { index, element ->
+        if (predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
@@ -136,8 +142,8 @@
 
 inline fun <T> IndexedListSet<T>.retainAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (!predicate(index, elementAt(index))) {
+    forEachReversedIndexed { index, element ->
+        if (!predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
index 2448ff0..43f18e2 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
@@ -21,8 +21,8 @@
 typealias IndexedMap<K, V> = ArrayMap<K, V>
 
 inline fun <K, V> IndexedMap<K, V>.allIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (!predicate(index, keyAt(index), valueAt(index))) {
+    forEachIndexed { index, key, value ->
+        if (!predicate(index, key, value)) {
             return false
         }
     }
@@ -30,8 +30,8 @@
 }
 
 inline fun <K, V> IndexedMap<K, V>.anyIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, keyAt(index), valueAt(index))) {
+    forEachIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
             return true
         }
     }
@@ -46,8 +46,8 @@
     }
 
 inline fun <K, V, R> IndexedMap<K, V>.firstNotNullOfOrNullIndexed(transform: (Int, K, V) -> R): R? {
-    for (index in 0 until size) {
-        transform(index, keyAt(index), valueAt(index))?.let { return it }
+    forEachIndexed { index, key, value ->
+        transform(index, key, value)?.let { return it }
     }
     return null
 }
@@ -64,6 +64,12 @@
     }
 }
 
+inline fun <K, V> IndexedMap<K, V>.forEachReversedIndexed(action: (Int, K, V) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, keyAt(index), valueAt(index))
+    }
+}
+
 inline fun <K, V> IndexedMap<K, V>.forEachValueIndexed(action: (Int, V) -> Unit) {
     for (index in 0 until size) {
         action(index, valueAt(index))
@@ -90,6 +96,15 @@
     remove(key)
 }
 
+inline fun <K, V> IndexedMap<K, V>.noneIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
+    forEachIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
+            return false
+        }
+    }
+    return true
+}
+
 @Suppress("NOTHING_TO_INLINE")
 inline fun <K, V> IndexedMap<K, V>.putWithDefault(key: K, value: V, defaultValue: V): V {
     val index = indexOfKey(key)
@@ -113,8 +128,8 @@
 
 inline fun <K, V> IndexedMap<K, V>.removeAllIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (predicate(index, keyAt(index), valueAt(index))) {
+    forEachReversedIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
             removeAt(index)
             isChanged = true
         }
@@ -124,8 +139,8 @@
 
 inline fun <K, V> IndexedMap<K, V>.retainAllIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (!predicate(index, keyAt(index), valueAt(index))) {
+    forEachReversedIndexed { index, key, value ->
+        if (!predicate(index, key, value)) {
             removeAt(index)
             isChanged = true
         }
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedSet.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedSet.kt
index faaa6d3..13fa31f 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedSet.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedSet.kt
@@ -21,8 +21,8 @@
 typealias IndexedSet<T> = ArraySet<T>
 
 inline fun <T> IndexedSet<T>.allIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (!predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (!predicate(index, element)) {
             return false
         }
     }
@@ -30,8 +30,8 @@
 }
 
 inline fun <T> IndexedSet<T>.anyIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return true
         }
     }
@@ -50,6 +50,12 @@
     }
 }
 
+inline fun <T> IndexedSet<T>.forEachReversedIndexed(action: (Int, T) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, elementAt(index))
+    }
+}
+
 inline val <T> IndexedSet<T>.lastIndex: Int
     get() = size - 1
 
@@ -63,8 +69,8 @@
 }
 
 inline fun <T> IndexedSet<T>.noneIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return false
         }
     }
@@ -82,8 +88,8 @@
 
 inline fun <T> IndexedSet<T>.removeAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (predicate(index, elementAt(index))) {
+    forEachReversedIndexed { index, element ->
+        if (predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
@@ -93,8 +99,8 @@
 
 inline fun <T> IndexedSet<T>.retainAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (!predicate(index, elementAt(index))) {
+    forEachReversedIndexed { index, element ->
+        if (!predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
diff --git a/services/permission/java/com/android/server/permission/access/collection/IntMap.kt b/services/permission/java/com/android/server/permission/access/collection/IntMap.kt
index 0044b73..e905567 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IntMap.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IntMap.kt
@@ -21,8 +21,8 @@
 typealias IntMap<T> = SparseArray<T>
 
 inline fun <T> IntMap<T>.allIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (!predicate(index, keyAt(index), valueAt(index))) {
+    forEachIndexed { index, key, value ->
+        if (!predicate(index, key, value)) {
             return false
         }
     }
@@ -30,8 +30,8 @@
 }
 
 inline fun <T> IntMap<T>.anyIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, keyAt(index), valueAt(index))) {
+    forEachIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
             return true
         }
     }
@@ -46,8 +46,8 @@
     }
 
 inline fun <T, R> IntMap<T>.firstNotNullOfOrNullIndexed(transform: (Int, Int, T) -> R): R? {
-    for (index in 0 until size) {
-        transform(index, keyAt(index), valueAt(index))?.let { return it }
+    forEachIndexed { index, key, value ->
+        transform(index, key, value)?.let { return it }
     }
     return null
 }
@@ -64,6 +64,12 @@
     }
 }
 
+inline fun <T> IntMap<T>.forEachReversedIndexed(action: (Int, Int, T) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, keyAt(index), valueAt(index))
+    }
+}
+
 inline fun <T> IntMap<T>.forEachValueIndexed(action: (Int, T) -> Unit) {
     for (index in 0 until size) {
         action(index, valueAt(index))
@@ -91,8 +97,8 @@
 }
 
 inline fun <T> IntMap<T>.noneIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, keyAt(index), valueAt(index))) {
+    forEachIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
             return false
         }
     }
@@ -120,10 +126,22 @@
     }
 }
 
+// SparseArray.removeReturnOld() is @hide, so a backup once we move to APIs.
+fun <T> IntMap<T>.removeReturnOld(key: Int): T? {
+    val index = indexOfKey(key)
+    return if (index >= 0) {
+        val oldValue = valueAt(index)
+        removeAt(index)
+        oldValue
+    } else {
+        null
+    }
+}
+
 inline fun <T> IntMap<T>.removeAllIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (predicate(index, keyAt(index), valueAt(index))) {
+    forEachReversedIndexed { index, key, value ->
+        if (predicate(index, key, value)) {
             removeAt(index)
             isChanged = true
         }
@@ -133,8 +151,8 @@
 
 inline fun <T> IntMap<T>.retainAllIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (!predicate(index, keyAt(index), valueAt(index))) {
+    forEachReversedIndexed { index, key, value ->
+        if (!predicate(index, key, value)) {
             removeAt(index)
             isChanged = true
         }
diff --git a/services/permission/java/com/android/server/permission/access/collection/IntSet.kt b/services/permission/java/com/android/server/permission/access/collection/IntSet.kt
index 0d75a4c..4717251 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IntSet.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IntSet.kt
@@ -54,8 +54,8 @@
 fun IntSet(values: IntArray): IntSet = IntSet().apply{ this += values }
 
 inline fun IntSet.allIndexed(predicate: (Int, Int) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (!predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (!predicate(index, element)) {
             return false
         }
     }
@@ -63,8 +63,8 @@
 }
 
 inline fun IntSet.anyIndexed(predicate: (Int, Int) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return true
         }
     }
@@ -77,6 +77,12 @@
     }
 }
 
+inline fun IntSet.forEachReversedIndexed(action: (Int, Int) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, elementAt(index))
+    }
+}
+
 inline val IntSet.lastIndex: Int
     get() = size - 1
 
@@ -89,8 +95,8 @@
 }
 
 inline fun IntSet.noneIndexed(predicate: (Int, Int) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, elementAt(index))) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return false
         }
     }
@@ -115,8 +121,8 @@
 
 inline fun IntSet.removeAllIndexed(predicate: (Int, Int) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (predicate(index, elementAt(index))) {
+    forEachReversedIndexed { index, element ->
+        if (predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
@@ -126,8 +132,8 @@
 
 inline fun IntSet.retainAllIndexed(predicate: (Int, Int) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (!predicate(index, elementAt(index))) {
+    forEachReversedIndexed { index, element ->
+        if (!predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
diff --git a/services/permission/java/com/android/server/permission/access/collection/List.kt b/services/permission/java/com/android/server/permission/access/collection/List.kt
index d35e69e..91f15bc 100644
--- a/services/permission/java/com/android/server/permission/access/collection/List.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/List.kt
@@ -17,8 +17,8 @@
 package com.android.server.permission.access.collection
 
 inline fun <T> List<T>.allIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (!predicate(index, this[index])) {
+    forEachIndexed { index, element ->
+        if (!predicate(index, element)) {
             return false
         }
     }
@@ -26,8 +26,8 @@
 }
 
 inline fun <T> List<T>.anyIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, this[index])) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return true
         }
     }
@@ -40,9 +40,15 @@
     }
 }
 
+inline fun <T> List<T>.forEachReversedIndexed(action: (Int, T) -> Unit) {
+    for (index in lastIndex downTo 0) {
+        action(index, this[index])
+    }
+}
+
 inline fun <T> List<T>.noneIndexed(predicate: (Int, T) -> Boolean): Boolean {
-    for (index in 0 until size) {
-        if (predicate(index, this[index])) {
+    forEachIndexed { index, element ->
+        if (predicate(index, element)) {
             return false
         }
     }
@@ -51,8 +57,8 @@
 
 inline fun <T> MutableList<T>.removeAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (predicate(index, this[index])) {
+    forEachReversedIndexed { index, element ->
+        if (predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
@@ -62,8 +68,8 @@
 
 inline fun <T> MutableList<T>.retainAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
     var isChanged = false
-    for (index in lastIndex downTo 0) {
-        if (!predicate(index, this[index])) {
+    forEachReversedIndexed { index, element ->
+        if (!predicate(index, element)) {
             removeAt(index)
             isChanged = true
         }
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt
index 1b05520..6b2b1856 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt
@@ -16,18 +16,468 @@
 
 package com.android.server.permission.access.permission
 
-object PermissionFlags {
-    const val INSTALL_GRANTED = 1 shl 0
-    const val INSTALL_REVOKED = 1 shl 1
-    const val PROTECTION_GRANTED = 1 shl 2
-    const val ROLE_GRANTED = 1 shl 3
-    // For permissions that are granted in other ways,
-    // ex: via an API or implicit permissions that inherit from granted install permissions
-    const val OTHER_GRANTED = 1 shl 4
-    // For the permissions that are implicit for the package
-    const val IMPLICIT = 1 shl 5
+import android.app.AppOpsManager
+import android.app.admin.DevicePolicyManager
+import android.content.pm.PackageManager
+import android.os.Build
+import android.permission.PermissionManager
+import com.android.server.permission.access.util.andInv
+import com.android.server.permission.access.util.hasAnyBit
+import com.android.server.permission.access.util.hasBits
 
+/**
+ * A set of internal permission flags that's better than the set of `FLAG_PERMISSION_*` constants on
+ * [PackageManager].
+ *
+ * The old binary permission state is now tracked by multiple `*_GRANTED` and `*_REVOKED` flags, so
+ * that:
+ *
+ * - With [INSTALL_GRANTED] and [INSTALL_REVOKED], we can now get rid of the old per-package
+ *   `areInstallPermissionsFixed` attribute and correctly track it per-permission, finally fixing
+ *   edge cases during module rollbacks.
+ *
+ * - With [LEGACY_GRANTED] and [IMPLICIT_GRANTED], we can now ensure that legacy permissions and
+ *   implicit permissions split from non-runtime permissions are never revoked, without checking
+ *   split permissions and package state everywhere slowly and in slightly different ways.
+ *
+ * - With [RESTRICTION_REVOKED], we can now get rid of the error-prone logic about revoking and
+ *   potentially re-granting permissions upon restriction state changes.
+ *
+ * Permission grants due to protection level are now tracked by [PROTECTION_GRANTED], and permission
+ * grants due to [PackageManager.grantRuntimePermission] are now tracked by [RUNTIME_GRANTED].
+ *
+ * The [PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED] and
+ * [PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED] flags are now unified into [IMPLICIT], and
+ * they can be differentiated by the presence of [LEGACY_GRANTED].
+ *
+ * The rest of the permission flags have a 1:1 mapping to the old `FLAG_PERMISSION_*` constants, and
+ * don't have any effect on the binary permission state.
+ */
+object PermissionFlags {
+    /**
+     * Permission flag for a normal permission that is granted at package installation.
+     */
+    const val INSTALL_GRANTED = 1 shl 0
+
+    /**
+     * Permission flag for a normal permission that is revoked at package installation.
+     *
+     * Normally packages that have already been installed cannot be granted new normal permissions
+     * until its next installation (update), so this flag helps track that the normal permission was
+     * revoked upon its most recent installation.
+     */
+    const val INSTALL_REVOKED = 1 shl 1
+
+    /**
+     * Permission flag for a signature or internal permission that is granted based on the
+     * permission's protection level, including its protection and protection flags.
+     *
+     * For example, this flag may be set when the permission is a signature permission and the
+     * package is having a compatible signing certificate with the package defining the permission,
+     * or when the permission is a privileged permission and the package is a privileged app with
+     * its permission in the
+     * [privileged permission allowlist](https://source.android.com/docs/core/permissions/perms-allowlist).
+     */
+    const val PROTECTION_GRANTED = 1 shl 2
+
+    /**
+     * Permission flag for a role or runtime permission that is or was granted by a role.
+     *
+     * @see PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE
+     */
+    const val ROLE = 1 shl 3
+
+    /**
+     * Permission flag for a development, role or runtime permission that is granted via
+     * [PackageManager.grantRuntimePermission].
+     */
+    const val RUNTIME_GRANTED = 1 shl 4
+
+    /**
+     * Permission flag for a runtime permission whose state is set by the user.
+     *
+     * For example, this flag may be set when the permission is allowed by the user in the
+     * request permission dialog, or managed in the permission settings.
+     *
+     * @see PackageManager.FLAG_PERMISSION_USER_SET
+     */
+    const val USER_SET = 1 shl 5
+
+    /**
+     * Permission flag for a runtime permission whose state is (revoked and) fixed by the user.
+     *
+     * For example, this flag may be set when the permission is denied twice by the user in the
+     * request permission dialog.
+     *
+     * @see PackageManager.FLAG_PERMISSION_USER_FIXED
+     */
+    const val USER_FIXED = 1 shl 6
+
+    /**
+     * Permission flag for a runtime permission whose state is set and fixed by the device policy
+     * via [DevicePolicyManager.setPermissionGrantState].
+     *
+     * @see PackageManager.FLAG_PERMISSION_POLICY_FIXED
+     */
+    const val POLICY_FIXED = 1 shl 7
+
+    /**
+     * Permission flag for a runtime permission that is (pregranted and) fixed by the system.
+     *
+     * For example, this flag may be set in
+     * [com.android.server.pm.permission.DefaultPermissionGrantPolicy].
+     *
+     * @see PackageManager.FLAG_PERMISSION_SYSTEM_FIXED
+     */
+    const val SYSTEM_FIXED = 1 shl 8
+
+    /**
+     * Permission flag for a runtime permission that is or was pregranted by the system.
+     *
+     * For example, this flag may be set in
+     * [com.android.server.pm.permission.DefaultPermissionGrantPolicy].
+     *
+     * @see PackageManager.FLAG_PERMISSION_SYSTEM_FIXED
+     */
+    const val PREGRANT = 1 shl 9
+
+    /**
+     * Permission flag for a runtime permission that is granted because the package targets a legacy
+     * SDK version before [Build.VERSION_CODES.M] and doesn't support runtime permissions.
+     *
+     * As long as this flag is set, the permission should always be considered granted, although
+     * [APP_OP_REVOKED] may cause the app op for the runtime permission to be revoked. Once the
+     * package targets a higher SDK version so that it started supporting runtime permissions, this
+     * flag should be removed and the remaining flags should take effect.
+     *
+     * @see PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED
+     * @see PackageManager.FLAG_PERMISSION_REVOKED_COMPAT
+     */
+    const val LEGACY_GRANTED = 1 shl 10
+
+    /**
+     * Permission flag for a runtime permission that is granted because the package targets a lower
+     * SDK version and the permission is implicit to it as a
+     * [split permission][PermissionManager.SplitPermissionInfo] from other non-runtime permissions.
+     *
+     * As long as this flag is set, the permission should always be considered granted, although
+     * [APP_OP_REVOKED] may cause the app op for the runtime permission to be revoked. Once the
+     * package targets a higher SDK version so that the permission is no longer implicit to it, this
+     * flag should be removed and the remaining flags should take effect.
+     *
+     * @see PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED
+     * @see PackageManager.FLAG_PERMISSION_REVOKED_COMPAT
+     */
+    const val IMPLICIT_GRANTED = 1 shl 11
+
+    /**
+     * Permission flag for a runtime permission that is granted because the package targets a legacy
+     * SDK version before [Build.VERSION_CODES.M] and doesn't support runtime permissions, so that
+     * it needs to be reviewed by the user; or granted because the package targets a lower SDK
+     * version and the permission is implicit to it as a
+     * [split permission][PermissionManager.SplitPermissionInfo] from other non-runtime permissions,
+     * so that it needs to be revoked when it's no longer implicit.
+     *
+     * @see PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED
+     * @see PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED
+     */
+    const val IMPLICIT = 1 shl 12
+
+    /**
+     * Permission flag for a runtime permission that is user-sensitive when it's granted.
+     *
+     * This flag is informational and managed by PermissionController.
+     *
+     * @see PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED
+     */
+    const val USER_SENSITIVE_WHEN_GRANTED = 1 shl 13
+
+    /**
+     * Permission flag for a runtime permission that is user-sensitive when it's revoked.
+     *
+     * This flag is informational and managed by PermissionController.
+     *
+     * @see PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED
+     */
+    const val USER_SENSITIVE_WHEN_REVOKED = 1 shl 14
+
+    /**
+     * Permission flag for a restricted runtime permission that is exempt by the package's
+     * installer.
+     *
+     * For example, this flag may be set when the installer applied the exemption as part of the
+     * [session parameters](https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setWhitelistedRestrictedPermissions(java.util.Set%3Cjava.lang.String%3E)).
+     *
+     * The permission will be restricted when none of the exempt flags is set.
+     *
+     * @see PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT
+     */
+    const val INSTALLER_EXEMPT = 1 shl 15
+
+    /**
+     * Permission flag for a restricted runtime permission that is exempt by the system.
+     *
+     * For example, this flag may be set when the package is a system app.
+     *
+     * The permission will be restricted when none of the exempt flags is set.
+     *
+     * @see PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT
+     */
+    const val SYSTEM_EXEMPT = 1 shl 16
+
+    /**
+     * Permission flag for a restricted runtime permission that is exempt due to system upgrade.
+     *
+     * For example, this flag may be set when the package was installed before the system was
+     * upgraded to [Build.VERSION_CODES.Q], when restricted permissions were introduced.
+     *
+     * The permission will be restricted when none of the exempt flags is set.
+     *
+     * @see PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT
+     */
+    const val UPGRADE_EXEMPT = 1 shl 17
+
+    /**
+     * Permission flag for a restricted runtime permission that is revoked due to being hard
+     * restricted.
+     *
+     * @see PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION
+     */
+    const val RESTRICTION_REVOKED = 1 shl 18
+
+    /**
+     * Permission flag for a restricted runtime permission that is soft restricted.
+     *
+     * @see PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION
+     */
+    const val SOFT_RESTRICTED = 1 shl 19
+
+    /**
+     * Permission flag for a runtime permission whose app op is revoked.
+     *
+     * For example, this flag may be set when the runtime permission is legacy or implicit but still
+     * "revoked" by the user in permission settings, or when the app op mode for the runtime
+     * permission is set to revoked via [AppOpsManager.setUidMode].
+     *
+     * @see PackageManager.FLAG_PERMISSION_REVOKED_COMPAT
+     */
+    const val APP_OP_REVOKED = 1 shl 20
+
+    /**
+     * Permission flag for a runtime permission that is granted as one-time.
+     *
+     * For example, this flag may be set when the user selected "Only this time" in the request
+     * permission dialog.
+     *
+     * This flag, along with other user decisions when it is set, should never be persisted, and
+     * should be removed once the permission is revoked.
+     *
+     * @see PackageManager.FLAG_PERMISSION_ONE_TIME
+     */
+    const val ONE_TIME = 1 shl 21
+
+    /**
+     * Permission flag for a runtime permission that was revoked due to app hibernation.
+     *
+     * This flag is informational and added by PermissionController, and should be removed once the
+     * permission is granted again.
+     *
+     * @see PackageManager.FLAG_PERMISSION_AUTO_REVOKED
+     */
+    const val HIBERNATION = 1 shl 22
+
+    /**
+     * Permission flag for a runtime permission that is selected by the user.
+     *
+     * For example, this flag may be set when one of the coarse/fine location accuracies is
+     * selected by the user.
+     *
+     * This flag is informational and managed by PermissionController.
+     *
+     * @see PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY
+     */
+    const val USER_SELECTED = 1 shl 23
+
+    /**
+     * Mask for all permission flags.
+     */
     const val MASK_ALL = 0.inv()
-    const val MASK_GRANTED = INSTALL_GRANTED or PROTECTION_GRANTED or OTHER_GRANTED or ROLE_GRANTED
-    const val MASK_RUNTIME = OTHER_GRANTED or IMPLICIT
+
+    /**
+     * Mask for all permission flags that may be applied to a runtime permission.
+     */
+    const val MASK_RUNTIME = ROLE or RUNTIME_GRANTED or USER_SET or USER_FIXED or POLICY_FIXED or
+        SYSTEM_FIXED or PREGRANT or LEGACY_GRANTED or IMPLICIT_GRANTED or IMPLICIT or
+        USER_SENSITIVE_WHEN_GRANTED or USER_SENSITIVE_WHEN_REVOKED or INSTALLER_EXEMPT or
+        SYSTEM_EXEMPT or UPGRADE_EXEMPT or RESTRICTION_REVOKED or SOFT_RESTRICTED or
+        APP_OP_REVOKED or ONE_TIME or HIBERNATION or USER_SELECTED
+
+    /**
+     * Mask for all API permission flags about permission restriction.
+     */
+    private const val API_MASK_RESTRICTION =
+        PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT or
+            PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT or
+            PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT or
+            PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION
+
+    /**
+     * Mask for all permission flags about permission restriction.
+     */
+    private const val MASK_RESTRICTION = INSTALLER_EXEMPT or SYSTEM_EXEMPT or
+        UPGRADE_EXEMPT or RESTRICTION_REVOKED or SOFT_RESTRICTED
+
+    fun isPermissionGranted(policyFlags: Int): Boolean {
+        if (policyFlags.hasBits(INSTALL_GRANTED)) {
+            return true
+        }
+        if (policyFlags.hasBits(INSTALL_REVOKED)) {
+            return false
+        }
+        if (policyFlags.hasBits(PROTECTION_GRANTED)) {
+            return true
+        }
+        if (policyFlags.hasBits(LEGACY_GRANTED) || policyFlags.hasBits(IMPLICIT_GRANTED)) {
+            return true
+        }
+        if (policyFlags.hasBits(RESTRICTION_REVOKED)) {
+            return false
+        }
+        return policyFlags.hasBits(RUNTIME_GRANTED)
+    }
+
+    fun isAppOpGranted(policyFlags: Int): Boolean =
+        isPermissionGranted(policyFlags) && !policyFlags.hasBits(APP_OP_REVOKED)
+
+    fun isReviewRequired(policyFlags: Int): Boolean =
+        policyFlags.hasBits(LEGACY_GRANTED) && policyFlags.hasBits(IMPLICIT)
+
+    fun toApiFlags(policyFlags: Int): Int {
+        var apiFlags = 0
+        if (policyFlags.hasBits(USER_SET)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SET
+        }
+        if (policyFlags.hasBits(USER_FIXED)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_FIXED
+        }
+        if (policyFlags.hasBits(POLICY_FIXED)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_POLICY_FIXED
+        }
+        if (policyFlags.hasBits(SYSTEM_FIXED)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_SYSTEM_FIXED
+        }
+        if (policyFlags.hasBits(PREGRANT)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT
+        }
+        if (policyFlags.hasBits(IMPLICIT)) {
+            apiFlags = apiFlags or if (policyFlags.hasBits(LEGACY_GRANTED)) {
+                PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED
+            } else {
+                PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED
+            }
+        }
+        if (policyFlags.hasBits(USER_SENSITIVE_WHEN_GRANTED)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED
+        }
+        if (policyFlags.hasBits(USER_SENSITIVE_WHEN_REVOKED)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED
+        }
+        if (policyFlags.hasBits(INSTALLER_EXEMPT)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT
+        }
+        if (policyFlags.hasBits(SYSTEM_EXEMPT)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT
+        }
+        if (policyFlags.hasBits(UPGRADE_EXEMPT)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT
+        }
+        if (policyFlags.hasBits(RESTRICTION_REVOKED) || policyFlags.hasBits(SOFT_RESTRICTED)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION
+        }
+        if (policyFlags.hasBits(ROLE)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE
+        }
+        if (policyFlags.hasBits(APP_OP_REVOKED)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_REVOKED_COMPAT
+        }
+        if (policyFlags.hasBits(ONE_TIME)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_ONE_TIME
+        }
+        if (policyFlags.hasBits(HIBERNATION)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_AUTO_REVOKED
+        }
+        if (policyFlags.hasBits(USER_SELECTED)) {
+            apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY
+        }
+        return apiFlags
+    }
+
+    fun setRuntimePermissionGranted(policyFlags: Int, isGranted: Boolean): Int =
+        if (isGranted) policyFlags or RUNTIME_GRANTED else policyFlags andInv RUNTIME_GRANTED
+
+    fun updatePolicyFlags(policyFlags: Int, apiFlagMask: Int, apiFlagValues: Int): Int {
+        check(!apiFlagMask.hasAnyBit(API_MASK_RESTRICTION)) {
+            "Permission flags about permission restriction can only be directly mutated by the" +
+                " policy"
+        }
+        val oldApiFlags = toApiFlags(policyFlags)
+        val newApiFlags = (oldApiFlags andInv apiFlagMask) or (apiFlagValues and apiFlagMask)
+        return toPolicyFlags(policyFlags, newApiFlags)
+    }
+
+    private fun toPolicyFlags(oldPolicyFlags: Int, apiFlags: Int): Int {
+        var policyFlags = 0
+        policyFlags = policyFlags or (oldPolicyFlags and INSTALL_GRANTED)
+        policyFlags = policyFlags or (oldPolicyFlags and INSTALL_REVOKED)
+        policyFlags = policyFlags or (oldPolicyFlags and PROTECTION_GRANTED)
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE)) {
+            policyFlags = policyFlags or ROLE
+        }
+        policyFlags = policyFlags or (oldPolicyFlags and RUNTIME_GRANTED)
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SET)) {
+            policyFlags = policyFlags or USER_SET
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_FIXED)) {
+            policyFlags = policyFlags or USER_FIXED
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_POLICY_FIXED)) {
+            policyFlags = policyFlags or POLICY_FIXED
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_SYSTEM_FIXED)) {
+            policyFlags = policyFlags or SYSTEM_FIXED
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT)) {
+            policyFlags = policyFlags or PREGRANT
+        }
+        policyFlags = policyFlags or (oldPolicyFlags and LEGACY_GRANTED)
+        policyFlags = policyFlags or (oldPolicyFlags and IMPLICIT_GRANTED)
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED) ||
+            apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED)) {
+            policyFlags = policyFlags or IMPLICIT
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED)) {
+            policyFlags = policyFlags or USER_SENSITIVE_WHEN_GRANTED
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED)) {
+            policyFlags = policyFlags or USER_SENSITIVE_WHEN_REVOKED
+        }
+        // FLAG_PERMISSION_APPLY_RESTRICTION can be either REVOKED_BY_RESTRICTION when the
+        // permission is hard restricted, or SOFT_RESTRICTED when the permission is soft restricted.
+        // However since we should never allow indirect mutation of restriction state, we can just
+        // get the flags about restriction from the old policy flags.
+        policyFlags = policyFlags or (oldPolicyFlags and MASK_RESTRICTION)
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVOKED_COMPAT)) {
+            policyFlags = policyFlags or APP_OP_REVOKED
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_ONE_TIME)) {
+            policyFlags = policyFlags or ONE_TIME
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_AUTO_REVOKED)) {
+            policyFlags = policyFlags or HIBERNATION
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY)) {
+            policyFlags = policyFlags or USER_SELECTED
+        }
+        return policyFlags
+    }
 }
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index 1e6996b..8201736 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -232,7 +232,12 @@
     }
 
     override fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int {
-        TODO("Not yet implemented")
+        // TODO: Implement permission checks.
+        val appId = 0
+        val flags = service.getState {
+            with(policy) { getPermissionFlags(appId, userId, permissionName) }
+        }
+        return PermissionFlags.toApiFlags(flags)
     }
 
     override fun isPermissionRevokedByPolicy(
@@ -244,7 +249,12 @@
     }
 
     override fun isPermissionsReviewRequired(packageName: String, userId: Int): Boolean {
-        TODO("Not yet implemented")
+        val packageState = packageManagerLocal.withUnfilteredSnapshot()
+            .use { it.packageStates[packageName] } ?: return false
+        val permissionFlags = service.getState {
+            with(policy) { getUidPermissionFlags(packageState.appId, userId) }
+        } ?: return false
+        return permissionFlags.anyIndexed { _, _, flags -> PermissionFlags.isReviewRequired(flags) }
     }
 
     override fun shouldShowRequestPermissionRationale(
@@ -435,7 +445,8 @@
         } else {
             intArrayOf(userId)
         }
-        userIds.forEach { service.onPackageInstalled(androidPackage.packageName, params, it) }
+        userIds.forEach { service.onPackageInstalled(androidPackage.packageName, it) }
+        // TODO: Handle params.
     }
 
     override fun onPackageUninstalled(
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 3d6d2ce..b2f52cc 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
@@ -37,7 +37,6 @@
 import com.android.server.permission.access.UidUri
 import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
 import com.android.server.permission.access.util.andInv
-import com.android.server.permission.access.util.hasAnyBit
 import com.android.server.permission.access.util.hasBits
 import com.android.server.pm.KnownPackages
 import com.android.server.pm.parsing.PackageInfoUtils
@@ -474,10 +473,10 @@
             // should only affect the other static flags, but not dynamic flags like development or
             // role. This may be useful in the case of an updated system app.
             if (permission.isDevelopment) {
-                newFlags = newFlags or (oldFlags and PermissionFlags.OTHER_GRANTED)
+                newFlags = newFlags or (oldFlags and PermissionFlags.RUNTIME_GRANTED)
             }
             if (permission.isRole) {
-                newFlags = newFlags or (oldFlags and PermissionFlags.ROLE_GRANTED)
+                newFlags = newFlags or (oldFlags and PermissionFlags.ROLE)
             }
             setPermissionFlags(appId, userId, permissionName, newFlags)
         } else if (permission.isRuntime) {
@@ -519,8 +518,8 @@
                     "Unknown source permission $sourcePermissionName in split permissions"
                 }
                 val sourceFlags = getPermissionFlags(appId, userId, sourcePermissionName)
-                val isSourceGranted = sourceFlags.hasAnyBit(PermissionFlags.MASK_GRANTED)
-                val isNewGranted = newFlags.hasAnyBit(PermissionFlags.MASK_GRANTED)
+                val isSourceGranted = PermissionFlags.isPermissionGranted(sourceFlags)
+                val isNewGranted = PermissionFlags.isPermissionGranted(newFlags)
                 val isGrantingNewFromRevoke = isSourceGranted && !isNewGranted
                 if (isSourceGranted == isNewGranted || isGrantingNewFromRevoke) {
                     if (isGrantingNewFromRevoke) {
@@ -528,7 +527,7 @@
                     }
                     newFlags = newFlags or (sourceFlags and PermissionFlags.MASK_RUNTIME)
                     if (!sourcePermission.isRuntime && isSourceGranted) {
-                        newFlags = newFlags or PermissionFlags.OTHER_GRANTED
+                        newFlags = newFlags or PermissionFlags.IMPLICIT_GRANTED
                     }
                 }
             }
@@ -597,11 +596,9 @@
             newState.systemState.privilegedPermissionAllowlistSourcePackageNames) {
             return true
         }
-        if (isInSystemConfigPrivAppPermissions(androidPackage, permission.name)) {
-            return true
-        }
-        if (isInSystemConfigPrivAppDenyPermissions(androidPackage, permission.name)) {
-            return false
+        val allowlistState = getPrivilegedPermissionAllowlistState(androidPackage, permission.name)
+        if (allowlistState != null) {
+            return allowlistState
         }
         // Updated system apps do not need to be allowlisted
         if (packageState.isUpdatedSystemApp) {
@@ -611,66 +608,51 @@
         return !RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE
     }
 
-    private fun MutateStateScope.isInSystemConfigPrivAppPermissions(
+    /**
+     * Get the whether a privileged permission is explicitly allowed or denied for a package in the
+     * allowlist, or `null` if it's not in the allowlist.
+     */
+    private fun MutateStateScope.getPrivilegedPermissionAllowlistState(
         androidPackage: AndroidPackage,
         permissionName: String
-    ): Boolean {
+    ): Boolean? {
+        val permissionAllowlist = newState.systemState.permissionAllowlist
         // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName. The below is only for
         //  passing compilation but won't actually work.
         //val apexModuleName = androidPackage.apexModuleName
         val apexModuleName = androidPackage.packageName
-        val systemState = newState.systemState
         val packageName = androidPackage.packageName
-        val permissionNames = when {
-            androidPackage.isVendor -> systemState.vendorPrivAppPermissions[packageName]
-            androidPackage.isProduct -> systemState.productPrivAppPermissions[packageName]
-            androidPackage.isSystemExt -> systemState.systemExtPrivAppPermissions[packageName]
+        return when {
+            androidPackage.isVendor -> permissionAllowlist.getVendorPrivilegedAppAllowlistState(
+                packageName, permissionName
+            )
+            androidPackage.isProduct -> permissionAllowlist.getProductPrivilegedAppAllowlistState(
+                packageName, permissionName
+            )
+            androidPackage.isSystemExt ->
+                permissionAllowlist.getSystemExtPrivilegedAppAllowlistState(
+                    packageName, permissionName
+                )
             apexModuleName != null -> {
-                val apexPrivAppPermissions = systemState.apexPrivAppPermissions[apexModuleName]
-                    ?.get(packageName)
-                val privAppPermissions = systemState.privAppPermissions[packageName]
-                when {
-                    apexPrivAppPermissions == null -> privAppPermissions
-                    privAppPermissions == null -> apexPrivAppPermissions
-                    else -> apexPrivAppPermissions + privAppPermissions
+                val nonApexAllowlistState = permissionAllowlist.getPrivilegedAppAllowlistState(
+                    packageName, permissionName
+                )
+                if (nonApexAllowlistState != null) {
+                    // TODO(andreionea): Remove check as soon as all apk-in-apex
+                    // permission allowlists are migrated.
+                    Log.w(
+                        LOG_TAG, "Package $packageName is an APK in APEX but has permission" +
+                            " allowlist on the system image, please bundle the allowlist in the" +
+                            " $apexModuleName APEX instead"
+                    )
                 }
+                val apexAllowlistState = permissionAllowlist.getApexPrivilegedAppAllowlistState(
+                    apexModuleName, packageName, permissionName
+                )
+                apexAllowlistState ?: nonApexAllowlistState
             }
-            else -> systemState.privAppPermissions[packageName]
+            else -> permissionAllowlist.getPrivilegedAppAllowlistState(packageName, permissionName)
         }
-        return permissionNames?.contains(permissionName) == true
-    }
-
-    private fun MutateStateScope.isInSystemConfigPrivAppDenyPermissions(
-        androidPackage: AndroidPackage,
-        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.
-        // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName. The below is only for
-        //  passing compilation but won't actually work.
-        //val apexModuleName = androidPackage.apexModuleName
-        val apexModuleName = androidPackage.packageName
-        val systemState = newState.systemState
-        val packageName = androidPackage.packageName
-        val permissionNames = when {
-            androidPackage.isVendor -> systemState.vendorPrivAppDenyPermissions[packageName]
-            androidPackage.isProduct -> systemState.productPrivAppDenyPermissions[packageName]
-            androidPackage.isSystemExt -> systemState.systemExtPrivAppDenyPermissions[packageName]
-            // Different from the previous implementation, which ignores the regular priv app
-            // denylist in this case, we now respect it as well to be consistent with the allowlist.
-            apexModuleName != null -> {
-                val apexPrivAppDenyPermissions = systemState
-                    .apexPrivAppDenyPermissions[apexModuleName]?.get(packageName)
-                val privAppDenyPermissions = systemState.privAppDenyPermissions[packageName]
-                when {
-                    apexPrivAppDenyPermissions == null -> privAppDenyPermissions
-                    privAppDenyPermissions == null -> apexPrivAppDenyPermissions
-                    else -> apexPrivAppDenyPermissions + privAppDenyPermissions
-                }
-            }
-            else -> systemState.privAppDenyPermissions[packageName]
-        }
-        return permissionNames?.contains(permissionName) == true
     }
 
     private fun MutateStateScope.anyPackageInAppId(
@@ -811,13 +793,13 @@
             }
             permission.isOem -> {
                 if (androidPackage.isOem) {
-                    val isOemAllowlisted = newState.systemState
-                        .oemPermissions[packageName]?.get(permissionName)
-                    checkNotNull(isOemAllowlisted) {
+                    val allowlistState = newState.systemState.permissionAllowlist
+                        .getOemAppAllowlistState(packageName, permissionName)
+                    checkNotNull(allowlistState) {
                         "OEM permission $permissionName requested by package" +
                             " $packageName must be explicitly declared granted or not"
                     }
-                    return isOemAllowlisted
+                    return allowlistState
                 }
             }
         }
@@ -853,6 +835,9 @@
     fun GetStateScope.getPermission(permissionName: String): Permission? =
         state.systemState.permissions[permissionName]
 
+    fun GetStateScope.getUidPermissionFlags(appId: Int, userId: Int): IndexedMap<String, Int>? =
+        state.userStates[userId]?.uidPermissionFlags?.get(appId)
+
     fun GetStateScope.getPermissionFlags(
         appId: Int,
         userId: Int,
@@ -871,7 +856,8 @@
         appId: Int,
         userId: Int,
         permissionName: String
-    ): Int = state.userStates[userId].uidPermissionFlags[appId].getWithDefault(permissionName, 0)
+    ): Int =
+        state.userStates[userId]?.uidPermissionFlags?.get(appId).getWithDefault(permissionName, 0)
 
     fun MutateStateScope.setPermissionFlags(
         appId: Int,
@@ -892,7 +878,7 @@
         val uidPermissionFlags = userState.uidPermissionFlags
         var permissionFlags = uidPermissionFlags[appId]
         val oldFlags = permissionFlags.getWithDefault(permissionName, 0)
-        val newFlags = (oldFlags andInv flagMask) or flagValues
+        val newFlags = (oldFlags andInv flagMask) or (flagValues and flagMask)
         if (oldFlags == newFlags) {
             return false
         }
diff --git a/services/proguard.flags b/services/proguard.flags
index 6cdf11c..ba4560f 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -78,6 +78,13 @@
 -keep,allowoptimization,allowaccessmodification class com.android.server.wm.** implements com.android.server.wm.DisplayAreaPolicy$Provider
 
 # JNI keep rules
+# The global keep rule for native methods allows stripping of such methods if they're unreferenced
+# in Java. However, because system_server explicitly registers these methods from native code,
+# stripping them in Java can cause runtime issues. As such, conservatively keep all such methods in
+# system_server subpackages as long as the containing class is also kept or referenced.
+-keepclassmembers class com.android.server.** {
+  native <methods>;
+}
 # TODO(b/210510433): Revisit and fix with @Keep, or consider auto-generating from
 # frameworks/base/services/core/jni/onload.cpp.
 -keep,allowoptimization,allowaccessmodification class com.android.server.broadcastradio.hal1.BroadcastRadioService { *; }
diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp
index cc26593..ebd6b64 100644
--- a/services/tests/PackageManagerServiceTests/server/Android.bp
+++ b/services/tests/PackageManagerServiceTests/server/Android.bp
@@ -94,7 +94,6 @@
         "libunwindstack",
         "libutils",
         "netd_aidl_interface-V5-cpp",
-        "libservices.core.settings.testonly",
     ],
 
     dxflags: ["--multi-dex"],
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/AppsFilterImplTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/BundleUtilsTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BundleUtilsTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/BundleUtilsTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BundleUtilsTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/CompatibilityModeTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CompatibilityModeTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/CompatibilityModeTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CompatibilityModeTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/CrossProfileAppsServiceImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/CrossProfileAppsServiceImplTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/InstallerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/InstallerTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/InstallerTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/InstallerTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetManagerServiceTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetManagerServiceTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetManagerServiceTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetManagerServiceTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetStrings.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetStrings.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetStrings.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetStrings.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetUtils.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetUtils.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetUtils.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetUtils.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/ModuleInfoProviderTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ModuleInfoProviderTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/ModuleInfoProviderTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ModuleInfoProviderTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageInstallerSessionTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageInstallerSessionTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerServiceTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerServiceTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
similarity index 99%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index b3f64b6..3727d66 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -111,10 +111,6 @@
     private static final String PACKAGE_NAME_3 = "com.android.app3";
     private static final int TEST_RESOURCE_ID = 2131231283;
 
-    static {
-        System.loadLibrary("services.core.settings.testonly");
-    }
-
     @Mock
     RuntimePermissionsPersistence mRuntimePermissionsPersistence;
     @Mock
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerTests.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageParserTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageSignaturesTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageSignaturesTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageSignaturesTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageSignaturesTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageUserStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageUserStateTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageVerificationStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageVerificationStateTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageVerificationStateTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageVerificationStateTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/ParallelPackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/ParallelPackageParserTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PreferredComponentTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PreferredComponentTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PreferredComponentTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PreferredComponentTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/RestrictionsSetTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/RestrictionsSetTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/RestrictionsSetTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/RestrictionsSetTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/ScanRequestBuilder.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanRequestBuilder.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/ScanRequestBuilder.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanRequestBuilder.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/ScanTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/ScanTests.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/UserDataPreparerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/UserDataPreparerTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/WatchedIntentHandlingTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/WatchedIntentHandlingTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/WatchedIntentHandlingTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/WatchedIntentHandlingTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/PackageParserLegacyCoreTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/PackageParserLegacyCoreTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/PackageParsingDeferErrorTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParsingDeferErrorTest.kt
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/PackageParsingDeferErrorTest.kt
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParsingDeferErrorTest.kt
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/SystemPartitionParseTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/SystemPartitionParseTest.kt
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/SystemPartitionParseTest.kt
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/SystemPartitionParseTest.kt
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidHidlUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidHidlUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidHidlUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidHidlUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidNetIpSecIkeUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidNetIpSecIkeUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidNetIpSecIkeUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidNetIpSecIkeUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidTestBaseUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidTestBaseUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidTestBaseUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidTestBaseUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidTestRunnerSplitUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidTestRunnerSplitUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidTestRunnerSplitUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidTestRunnerSplitUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/ApexSharedLibraryUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/ApexSharedLibraryUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/ApexSharedLibraryUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/ApexSharedLibraryUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/ComGoogleAndroidMapsUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/ComGoogleAndroidMapsUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/ComGoogleAndroidMapsUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/ComGoogleAndroidMapsUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/OptionalClassRunner.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/OptionalClassRunner.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/OptionalClassRunner.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/OptionalClassRunner.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/OrgApacheHttpLegacyUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/OrgApacheHttpLegacyUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/OrgApacheHttpLegacyUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/OrgApacheHttpLegacyUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/PackageBackwardCompatibilityTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/PackageBackwardCompatibilityTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/PackageBackwardCompatibilityTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/PackageBackwardCompatibilityTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/PackageSharedLibraryUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/PackageSharedLibraryUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/PackageSharedLibraryUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/PackageSharedLibraryUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/RemoveUnnecessaryAndroidTestBaseLibraryTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/RemoveUnnecessaryAndroidTestBaseLibraryTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/RemoveUnnecessaryAndroidTestBaseLibraryTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/RemoveUnnecessaryAndroidTestBaseLibraryTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/RemoveUnnecessaryOrgApacheHttpLegacyLibraryTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/RemoveUnnecessaryOrgApacheHttpLegacyLibraryTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/RemoveUnnecessaryOrgApacheHttpLegacyLibraryTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/RemoveUnnecessaryOrgApacheHttpLegacyLibraryTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/OWNERS b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/OWNERS
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/OWNERS
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/OWNERS
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/WatchableTester.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/WatchableTester.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/WatchableTester.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/WatchableTester.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/WatcherTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/WatcherTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/WatcherTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/WatcherTest.java
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java
index be13bad..021d01c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java
@@ -48,7 +48,7 @@
     StaticMockitoSession mSession;
 
     @Mock
-    AppOpsServiceImpl.Constants mConstants;
+    AppOpsService.Constants mConstants;
 
     @Mock
     Context mContext;
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsServiceTest.java
index 7d4bc6f..c0688d1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsServiceTest.java
@@ -22,8 +22,6 @@
 import static android.app.AppOpsManager.OP_READ_SMS;
 import static android.app.AppOpsManager.OP_WIFI_SCAN;
 import static android.app.AppOpsManager.OP_WRITE_SMS;
-import static android.app.AppOpsManager.resolvePackageName;
-import static android.os.Process.INVALID_UID;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -41,7 +39,6 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 
-import android.app.AppOpsManager;
 import android.app.AppOpsManager.OpEntry;
 import android.app.AppOpsManager.PackageOps;
 import android.content.ContentResolver;
@@ -89,13 +86,13 @@
 
     private File mAppOpsFile;
     private Handler mHandler;
-    private AppOpsServiceImpl mAppOpsService;
+    private AppOpsService mAppOpsService;
     private int mMyUid;
     private long mTestStartMillis;
     private StaticMockitoSession mMockingSession;
 
     private void setupAppOpsService() {
-        mAppOpsService = new AppOpsServiceImpl(mAppOpsFile, mHandler, spy(sContext));
+        mAppOpsService = new AppOpsService(mAppOpsFile, mHandler, spy(sContext));
         mAppOpsService.mHistoricalRegistry.systemReady(sContext.getContentResolver());
 
         // Always approve all permission checks
@@ -164,20 +161,17 @@
 
     @Test
     public void testNoteOperationAndGetOpsForPackage() {
-        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null);
-        mAppOpsService.setMode(OP_WRITE_SMS, mMyUid, sMyPackageName, MODE_ERRORED, null);
+        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED);
+        mAppOpsService.setMode(OP_WRITE_SMS, mMyUid, sMyPackageName, MODE_ERRORED);
 
         // Note an op that's allowed.
-        mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
+        mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false);
         List<PackageOps> loggedOps = getLoggedOps();
         assertContainsOp(loggedOps, OP_READ_SMS, mTestStartMillis, -1, MODE_ALLOWED);
 
         // Note another op that's not allowed.
-        mAppOpsService.noteOperationUnchecked(OP_WRITE_SMS, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
+        mAppOpsService.noteOperation(OP_WRITE_SMS, mMyUid, sMyPackageName, null, false, null,
+                false);
         loggedOps = getLoggedOps();
         assertContainsOp(loggedOps, OP_READ_SMS, mTestStartMillis, -1, MODE_ALLOWED);
         assertContainsOp(loggedOps, OP_WRITE_SMS, -1, mTestStartMillis, MODE_ERRORED);
@@ -191,20 +185,18 @@
     @Test
     public void testNoteOperationAndGetOpsForPackage_controlledByDifferentOp() {
         // This op controls WIFI_SCAN
-        mAppOpsService.setMode(OP_COARSE_LOCATION, mMyUid, sMyPackageName, MODE_ALLOWED, null);
+        mAppOpsService.setMode(OP_COARSE_LOCATION, mMyUid, sMyPackageName, MODE_ALLOWED);
 
-        assertThat(mAppOpsService.noteOperationUnchecked(OP_WIFI_SCAN, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF)).isEqualTo(MODE_ALLOWED);
+        assertThat(mAppOpsService.noteOperation(OP_WIFI_SCAN, mMyUid, sMyPackageName, null, false,
+                null, false).getOpMode()).isEqualTo(MODE_ALLOWED);
 
         assertContainsOp(getLoggedOps(), OP_WIFI_SCAN, mTestStartMillis, -1,
                 MODE_ALLOWED /* default for WIFI_SCAN; this is not changed or used in this test */);
 
         // Now set COARSE_LOCATION to ERRORED -> this will make WIFI_SCAN disabled as well.
-        mAppOpsService.setMode(OP_COARSE_LOCATION, mMyUid, sMyPackageName, MODE_ERRORED, null);
-        assertThat(mAppOpsService.noteOperationUnchecked(OP_WIFI_SCAN, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF)).isEqualTo(MODE_ERRORED);
+        mAppOpsService.setMode(OP_COARSE_LOCATION, mMyUid, sMyPackageName, MODE_ERRORED);
+        assertThat(mAppOpsService.noteOperation(OP_WIFI_SCAN, mMyUid, sMyPackageName, null, false,
+                null, false).getOpMode()).isEqualTo(MODE_ERRORED);
 
         assertContainsOp(getLoggedOps(), OP_WIFI_SCAN, mTestStartMillis, mTestStartMillis,
                 MODE_ALLOWED /* default for WIFI_SCAN; this is not changed or used in this test */);
@@ -213,14 +205,11 @@
     // Tests the dumping and restoring of the in-memory state to/from XML.
     @Test
     public void testStatePersistence() {
-        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null);
-        mAppOpsService.setMode(OP_WRITE_SMS, mMyUid, sMyPackageName, MODE_ERRORED, null);
-        mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
-        mAppOpsService.noteOperationUnchecked(OP_WRITE_SMS, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
+        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED);
+        mAppOpsService.setMode(OP_WRITE_SMS, mMyUid, sMyPackageName, MODE_ERRORED);
+        mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false);
+        mAppOpsService.noteOperation(OP_WRITE_SMS, mMyUid, sMyPackageName, null, false, null,
+                false);
         mAppOpsService.writeState();
 
         // Create a new app ops service which will initialize its state from XML.
@@ -235,10 +224,8 @@
     // Tests that ops are persisted during shutdown.
     @Test
     public void testShutdown() {
-        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null);
-        mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
+        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED);
+        mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false);
         mAppOpsService.shutdown();
 
         // Create a new app ops service which will initialize its state from XML.
@@ -251,10 +238,8 @@
 
     @Test
     public void testGetOpsForPackage() {
-        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null);
-        mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
+        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED);
+        mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false);
 
         // Query all ops
         List<PackageOps> loggedOps = mAppOpsService.getOpsForPackage(
@@ -282,10 +267,8 @@
 
     @Test
     public void testPackageRemoved() {
-        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null);
-        mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
+        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED);
+        mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false);
 
         List<PackageOps> loggedOps = getLoggedOps();
         assertContainsOp(loggedOps, OP_READ_SMS, mTestStartMillis, -1, MODE_ALLOWED);
@@ -339,10 +322,8 @@
 
     @Test
     public void testUidRemoved() {
-        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null);
-        mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid,
-                resolvePackageName(mMyUid, sMyPackageName), null,
-                INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF);
+        mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED);
+        mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false);
 
         List<PackageOps> loggedOps = getLoggedOps();
         assertContainsOp(loggedOps, OP_READ_SMS, mTestStartMillis, -1, MODE_ALLOWED);
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
index 3efd5e7..98e895a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
@@ -76,7 +76,7 @@
     ActivityManagerInternal mAmi;
 
     @Mock
-    AppOpsServiceImpl.Constants mConstants;
+    AppOpsService.Constants mConstants;
 
     AppOpsUidStateTrackerTestExecutor mExecutor = new AppOpsUidStateTrackerTestExecutor();
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
index 302fa0f..9eed6ad 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
@@ -121,13 +121,13 @@
         }
     }
 
-    private void assertSameModes(SparseArray<AppOpsServiceImpl.UidState> uidStates,
+    private void assertSameModes(SparseArray<AppOpsService.UidState> uidStates,
             int op1, int op2) {
         int numberOfNonDefaultOps = 0;
         final int defaultModeOp1 = AppOpsManager.opToDefaultMode(op1);
         final int defaultModeOp2 = AppOpsManager.opToDefaultMode(op2);
         for (int i = 0; i < uidStates.size(); i++) {
-            final AppOpsServiceImpl.UidState uidState = uidStates.valueAt(i);
+            final AppOpsService.UidState uidState = uidStates.valueAt(i);
             SparseIntArray opModes = uidState.getNonDefaultUidModes();
             if (opModes != null) {
                 final int uidMode1 = opModes.get(op1, defaultModeOp1);
@@ -141,12 +141,12 @@
                 continue;
             }
             for (int j = 0; j < uidState.pkgOps.size(); j++) {
-                final AppOpsServiceImpl.Ops ops = uidState.pkgOps.valueAt(j);
+                final AppOpsService.Ops ops = uidState.pkgOps.valueAt(j);
                 if (ops == null) {
                     continue;
                 }
-                final AppOpsServiceImpl.Op _op1 = ops.get(op1);
-                final AppOpsServiceImpl.Op _op2 = ops.get(op2);
+                final AppOpsService.Op _op1 = ops.get(op1);
+                final AppOpsService.Op _op2 = ops.get(op2);
                 final int mode1 = (_op1 == null) ? defaultModeOp1 : _op1.getMode();
                 final int mode2 = (_op2 == null) ? defaultModeOp2 : _op2.getMode();
                 assertEquals(mode1, mode2);
@@ -199,7 +199,7 @@
     public void upgradeRunAnyInBackground() {
         extractAppOpsFile(APP_OPS_UNVERSIONED_ASSET_PATH);
 
-        AppOpsServiceImpl testService = new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext);
+        AppOpsService testService = new AppOpsService(sAppOpsFile, mHandler, mTestContext);
 
         testService.upgradeRunAnyInBackgroundLocked();
         assertSameModes(testService.mUidStates, AppOpsManager.OP_RUN_IN_BACKGROUND,
@@ -244,7 +244,7 @@
             return UserHandle.getUid(userId, appIds[index]);
         }).when(mPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt());
 
-        AppOpsServiceImpl testService = new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext);
+        AppOpsService testService = new AppOpsService(sAppOpsFile, mHandler, mTestContext);
 
         testService.upgradeScheduleExactAlarmLocked();
 
@@ -259,7 +259,7 @@
                 } else {
                     expectedMode = previousMode;
                 }
-                final AppOpsServiceImpl.UidState uidState = testService.mUidStates.get(uid);
+                final AppOpsService.UidState uidState = testService.mUidStates.get(uid);
                 assertEquals(expectedMode, uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM));
             }
         }
@@ -268,7 +268,7 @@
         int[] unrelatedUidsInFile = {10225, 10178};
 
         for (int uid : unrelatedUidsInFile) {
-            final AppOpsServiceImpl.UidState uidState = testService.mUidStates.get(uid);
+            final AppOpsService.UidState uidState = testService.mUidStates.get(uid);
             assertEquals(AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM),
                     uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM));
         }
@@ -278,8 +278,8 @@
     public void upgradeFromNoFile() {
         assertFalse(sAppOpsFile.exists());
 
-        AppOpsServiceImpl testService = spy(
-                new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext));
+        AppOpsService testService = spy(
+                new AppOpsService(sAppOpsFile, mHandler, mTestContext));
 
         doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
         doNothing().when(testService).upgradeScheduleExactAlarmLocked();
@@ -296,7 +296,7 @@
 
         AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile);
         assertTrue(parser.parse());
-        assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion);
+        assertEquals(AppOpsService.CURRENT_VERSION, parser.mVersion);
     }
 
     @Test
@@ -306,8 +306,8 @@
         assertTrue(parser.parse());
         assertEquals(AppOpsDataParser.NO_VERSION, parser.mVersion);
 
-        AppOpsServiceImpl testService = spy(
-                new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext));
+        AppOpsService testService = spy(
+                new AppOpsService(sAppOpsFile, mHandler, mTestContext));
 
         doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
         doNothing().when(testService).upgradeScheduleExactAlarmLocked();
@@ -320,7 +320,7 @@
 
         testService.writeState();
         assertTrue(parser.parse());
-        assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion);
+        assertEquals(AppOpsService.CURRENT_VERSION, parser.mVersion);
     }
 
     @Test
@@ -330,8 +330,8 @@
         assertTrue(parser.parse());
         assertEquals(1, parser.mVersion);
 
-        AppOpsServiceImpl testService = spy(
-                new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext));
+        AppOpsService testService = spy(
+                new AppOpsService(sAppOpsFile, mHandler, mTestContext));
 
         doNothing().when(testService).upgradeRunAnyInBackgroundLocked();
         doNothing().when(testService).upgradeScheduleExactAlarmLocked();
@@ -344,7 +344,7 @@
 
         testService.writeState();
         assertTrue(parser.parse());
-        assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion);
+        assertEquals(AppOpsService.CURRENT_VERSION, parser.mVersion);
     }
 
     /**
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java
new file mode 100644
index 0000000..880501f
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.state;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.display.DisplayManagerInternal;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.display.DisplayPowerProximityStateController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class DisplayStateControllerTest {
+    private static final boolean DISPLAY_ENABLED = true;
+    private static final boolean DISPLAY_IN_TRANSITION = true;
+
+    private DisplayStateController mDisplayStateController;
+
+    @Mock
+    private DisplayPowerProximityStateController mDisplayPowerProximityStateController;
+
+    @Before
+    public void before() {
+        MockitoAnnotations.initMocks(this);
+        mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
+    }
+
+    @Test
+    public void updateProximityStateEvaluatesStateOffPolicyAsExpected() {
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                false);
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+
+        displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
+        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
+                !DISPLAY_IN_TRANSITION);
+        assertEquals(Display.STATE_OFF, state);
+        verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
+                Display.STATE_OFF);
+        assertEquals(true, mDisplayStateController.shouldPerformScreenOffTransition());
+    }
+
+    @Test
+    public void updateProximityStateEvaluatesDozePolicyAsExpected() {
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                false);
+        validDisplayState(DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE,
+                Display.STATE_DOZE, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+    }
+
+    @Test
+    public void updateProximityStateEvaluatesDimPolicyAsExpected() {
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                false);
+        validDisplayState(DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM,
+                Display.STATE_ON, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+    }
+
+    @Test
+    public void updateProximityStateEvaluatesDimBrightAsExpected() {
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                false);
+        validDisplayState(DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT,
+                Display.STATE_ON, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+    }
+
+    @Test
+    public void updateProximityStateWorksAsExpectedWhenDisplayDisabled() {
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                false);
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+
+        displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
+        int state = mDisplayStateController.updateDisplayState(displayPowerRequest,
+                !DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+        assertEquals(Display.STATE_OFF, state);
+        verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
+                Display.STATE_ON);
+        assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
+    }
+
+    @Test
+    public void updateProximityStateWorksAsExpectedWhenTransitionPhase() {
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                false);
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+
+        displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
+        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
+                DISPLAY_IN_TRANSITION);
+        assertEquals(Display.STATE_OFF, state);
+        verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
+                Display.STATE_ON);
+        assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
+    }
+
+    @Test
+    public void updateProximityStateWorksAsExpectedWhenScreenOffBecauseOfProximity() {
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                true);
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+
+        displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
+        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
+                !DISPLAY_IN_TRANSITION);
+        assertEquals(Display.STATE_OFF, state);
+        verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
+                Display.STATE_ON);
+        assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
+    }
+
+    private void validDisplayState(int policy, int displayState, boolean isEnabled,
+            boolean isInTransition) {
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+        displayPowerRequest.policy = policy;
+        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, isEnabled,
+                isInTransition);
+        assertEquals(displayState, state);
+        verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
+                displayState);
+        assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
index a9dc4af..480a4f3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
@@ -217,7 +217,7 @@
         assertEquals(0, preferredUidOnly.size());
         assertEquals(0, stoppable.size());
         assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
-        assertEquals(0, assignmentInfo.numRunningTopEj);
+        assertEquals(0, assignmentInfo.numRunningImmediacyPrivileged);
     }
 
     @Test
@@ -239,7 +239,7 @@
         assertEquals(0, preferredUidOnly.size());
         assertEquals(0, stoppable.size());
         assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
-        assertEquals(0, assignmentInfo.numRunningTopEj);
+        assertEquals(0, assignmentInfo.numRunningImmediacyPrivileged);
     }
 
     @Test
@@ -265,15 +265,14 @@
         assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size());
         assertEquals(0, stoppable.size());
         assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
-        assertEquals(0, assignmentInfo.numRunningTopEj);
+        assertEquals(0, assignmentInfo.numRunningImmediacyPrivileged);
     }
 
     @Test
-    public void testPrepareForAssignmentDetermination_onlyRunningTopEjs() {
+    public void testPrepareForAssignmentDetermination_onlyStartedWithImmediacyPrivilege() {
         for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) {
             JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i);
-            job.startedAsExpeditedJob = true;
-            job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP;
+            job.startedWithImmediacyPrivilege = true;
             mJobConcurrencyManager.addRunningJobForTesting(job);
         }
 
@@ -294,7 +293,7 @@
         assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT / 2, stoppable.size());
         assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
         assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT,
-                assignmentInfo.numRunningTopEj);
+                assignmentInfo.numRunningImmediacyPrivileged);
     }
 
     @Test
@@ -500,6 +499,38 @@
     }
 
     @Test
+    public void testHasImmediacyPrivilege() {
+        JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE, 0);
+        spyOn(job);
+        assertFalse(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job));
+
+        doReturn(false).when(job).shouldTreatAsExpeditedJob();
+        doReturn(false).when(job).shouldTreatAsUserInitiated();
+        job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP;
+        assertFalse(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job));
+
+        doReturn(true).when(job).shouldTreatAsExpeditedJob();
+        doReturn(false).when(job).shouldTreatAsUserInitiated();
+        job.lastEvaluatedBias = JobInfo.BIAS_DEFAULT;
+        assertFalse(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job));
+
+        doReturn(false).when(job).shouldTreatAsExpeditedJob();
+        doReturn(true).when(job).shouldTreatAsUserInitiated();
+        job.lastEvaluatedBias = JobInfo.BIAS_DEFAULT;
+        assertFalse(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job));
+
+        doReturn(false).when(job).shouldTreatAsExpeditedJob();
+        doReturn(true).when(job).shouldTreatAsUserInitiated();
+        job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP;
+        assertTrue(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job));
+
+        doReturn(true).when(job).shouldTreatAsExpeditedJob();
+        doReturn(false).when(job).shouldTreatAsUserInitiated();
+        job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP;
+        assertTrue(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job));
+    }
+
+    @Test
     public void testIsPkgConcurrencyLimited_top() {
         final JobStatus topJob = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE, 0);
         topJob.lastEvaluatedBias = JobInfo.BIAS_TOP_APP;
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
index 27d0662..4f56271 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
@@ -193,6 +193,7 @@
         val incrementalManager: IncrementalManager = mock()
         val platformCompat: PlatformCompat = mock()
         val settings: Settings = mock()
+        val crossProfileIntentFilterHelper: CrossProfileIntentFilterHelper = mock()
         val resources: Resources = mock()
         val systemConfig: SystemConfig = mock()
         val apexManager: ApexManager = mock()
@@ -279,6 +280,8 @@
         whenever(mocks.injector.incrementalManager).thenReturn(mocks.incrementalManager)
         whenever(mocks.injector.compatibility).thenReturn(mocks.platformCompat)
         whenever(mocks.injector.settings).thenReturn(mocks.settings)
+        whenever(mocks.injector.crossProfileIntentFilterHelper)
+                .thenReturn(mocks.crossProfileIntentFilterHelper)
         whenever(mocks.injector.dexManager).thenReturn(mocks.dexManager)
         whenever(mocks.injector.systemConfig).thenReturn(mocks.systemConfig)
         whenever(mocks.injector.apexManager).thenReturn(mocks.apexManager)
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
index 6d8910e..58cff94 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
@@ -107,8 +107,8 @@
                 onVisible(PROFILE_USER_ID));
         startForegroundUser(PARENT_USER_ID);
 
-        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
-                DEFAULT_DISPLAY);
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
 
         expectUserIsVisible(PROFILE_USER_ID);
@@ -138,7 +138,8 @@
     public void testStartBgUser_onInvalidDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, INVALID_DISPLAY);
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+                INVALID_DISPLAY);
 
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
 
@@ -151,7 +152,7 @@
     public void testStartBgUser_onSecondaryDisplay_displayAvailable() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
 
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
                 SECONDARY_DISPLAY_ID);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
 
@@ -176,7 +177,7 @@
         expectUserIsNotVisibleOnDisplay("before", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
         expectUserIsNotVisibleOnDisplay("before", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
 
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
                 SECONDARY_DISPLAY_ID);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
 
@@ -189,7 +190,7 @@
         AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(OTHER_USER_ID));
         startUserInSecondaryDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
 
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
                 SECONDARY_DISPLAY_ID);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
 
@@ -205,7 +206,7 @@
         AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
         startUserInSecondaryDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
 
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
                 SECONDARY_DISPLAY_ID);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
 
@@ -228,8 +229,8 @@
         AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID));
         startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
 
-        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
-                DEFAULT_DISPLAY);
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
 
         expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
index 1065392..3d64c29 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
@@ -106,8 +106,8 @@
                 onVisible(PROFILE_USER_ID));
         startForegroundUser(PARENT_USER_ID);
 
-        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
-                DEFAULT_DISPLAY);
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
 
         expectUserIsVisible(PROFILE_USER_ID);
@@ -126,7 +126,7 @@
     public void testStartBgUser_onSecondaryDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
                 SECONDARY_DISPLAY_ID);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
index 4487d13..74fd9ff 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -26,6 +26,9 @@
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND;
+import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND_VISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_START_MODE_FOREGROUND;
 import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString;
 import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
 import static com.android.server.pm.UserVisibilityChangedEvent.onVisible;
@@ -99,8 +102,9 @@
      */
     protected static final int OTHER_SECONDARY_DISPLAY_ID = 108;
 
-    protected static final boolean FG = true;
-    protected static final boolean BG = false;
+    protected static final int FG = USER_START_MODE_FOREGROUND;
+    protected static final int BG = USER_START_MODE_BACKGROUND;
+    protected static final int BG_VISIBLE = USER_START_MODE_BACKGROUND_VISIBLE;
 
     private Handler mHandler;
     protected AsyncUserVisibilityListener.Factory mListenerFactory;
@@ -154,8 +158,7 @@
     public final void testStartBgUser_onDefaultDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
-                DEFAULT_DISPLAY);
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, DEFAULT_DISPLAY);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
 
         expectUserIsNotVisibleAtAll(USER_ID);
@@ -166,6 +169,34 @@
     }
 
     @Test
+    public final void testStartBgUser_onDefaultDisplay_visible() throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+                DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
+        expectNoDisplayAssignedToUser(PROFILE_USER_ID);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testStartBgUser_onSecondaryDisplay_invisible() throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
+                SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
+        expectNoDisplayAssignedToUser(PROFILE_USER_ID);
+
+        listener.verify();
+    }
+
+    @Test
     public final void testStartBgSystemUser_onSecondaryDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForEvents(
                 onInvisible(INITIAL_CURRENT_USER_ID),
@@ -228,8 +259,8 @@
             throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
-        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
-                DEFAULT_DISPLAY);
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
 
         expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
@@ -244,8 +275,8 @@
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
         startBackgroundUser(PARENT_USER_ID);
 
-        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
-                DEFAULT_DISPLAY);
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
 
         expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
@@ -261,6 +292,21 @@
     public final void testStartBgProfile_onSecondaryDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
+        expectNoDisplayAssignedToUser(PROFILE_USER_ID);
+        expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testStartBgProfile_onSecondaryDisplay_invisible() throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
         int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
                 SECONDARY_DISPLAY_ID);
         assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
@@ -384,8 +430,8 @@
         Log.d(TAG, "starting default profile (" + PROFILE_USER_ID + ") in background after starting"
                 + " its parent (" + PARENT_USER_ID + ") on foreground");
 
-        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
-                DEFAULT_DISPLAY);
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
         if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) {
             throw new IllegalStateException("Failed to start profile user " + PROFILE_USER_ID
                     + ": mediator returned " + userAssignmentResultToString(result));
@@ -403,7 +449,7 @@
         Preconditions.checkArgument(displayId != INVALID_DISPLAY && displayId != DEFAULT_DISPLAY,
                 "must pass a secondary display, not %d", displayId);
         Log.d(TAG, "startUserInSecondaryDisplay(" + userId + ", " + displayId + ")");
-        int result = mMediator.assignUserToDisplayOnStart(userId, userId, BG, displayId);
+        int result = mMediator.assignUserToDisplayOnStart(userId, userId, BG_VISIBLE, displayId);
         if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) {
             throw new IllegalStateException("Failed to startuser " + userId
                     + " on background: mediator returned " + userAssignmentResultToString(result));
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 98037d7..0dfe664 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -243,14 +243,6 @@
     }
 
     @Test
-    public void testStartUserOnSecondaryDisplay_defaultDisplay() {
-        assertThrows(IllegalArgumentException.class, () -> mUserController
-                .startUserOnSecondaryDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY));
-
-        verifyUserNeverAssignedToDisplay();
-    }
-
-    @Test
     public void testStartUserOnSecondaryDisplay() {
         boolean started = mUserController.startUserOnSecondaryDisplay(TEST_USER_ID, 42);
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index d2f2af1..9c7c574 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.startsWith;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -143,4 +144,38 @@
         verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
     }
 
+    @Test
+    public void createNavigationTouchpad_hasDeviceId() {
+        final IBinder deviceToken = new Binder();
+        mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
+                deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
+
+        int deviceId = mInputController.getInputDeviceId(deviceToken);
+        int[] deviceIds = InputManager.getInstance().getInputDeviceIds();
+
+        assertWithMessage("InputManager's deviceIds list should contain id of the device").that(
+            deviceIds).asList().contains(deviceId);
+    }
+
+    @Test
+    public void createNavigationTouchpad_setsTypeAssociation() {
+        final IBinder deviceToken = new Binder();
+        mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
+                deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
+
+        verify(mInputManagerInternalMock).setTypeAssociation(
+                startsWith("virtualNavigationTouchpad:"), eq("touchNavigation"));
+    }
+
+    @Test
+    public void createAndUnregisterNavigationTouchpad_unsetsTypeAssociation() {
+        final IBinder deviceToken = new Binder();
+        mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
+                deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
+
+        mInputController.unregisterInputDevice(deviceToken);
+
+        verify(mInputManagerInternalMock).unsetTypeAssociation(
+                startsWith("virtualNavigationTouchpad:"));
+    }
 }
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 62ef523..31e53d5 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
@@ -69,6 +69,7 @@
 import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
 import android.hardware.input.VirtualTouchEvent;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.net.MacAddress;
@@ -97,6 +98,8 @@
 import com.android.server.input.InputManagerInternal;
 import com.android.server.sensors.SensorManagerInternal;
 
+import com.google.android.collect.Sets;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -108,6 +111,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Consumer;
 
 @Presubmit
@@ -124,12 +128,13 @@
     private static final String GOOGLE_MAPS_PACKAGE_NAME = "com.google.android.apps.maps";
     private static final String DEVICE_NAME = "device name";
     private static final int DISPLAY_ID = 2;
+    private static final int DISPLAY_ID_2 = 3;
+    private static final int DEVICE_OWNER_UID_1 = 50;
+    private static final int DEVICE_OWNER_UID_2 = 51;
     private static final int UID_1 = 0;
     private static final int UID_2 = 10;
     private static final int UID_3 = 10000;
     private static final int UID_4 = 10001;
-    private static final int ASSOCIATION_ID_1 = 1;
-    private static final int ASSOCIATION_ID_2 = 2;
     private static final int PRODUCT_ID = 10;
     private static final int VENDOR_ID = 5;
     private static final String UNIQUE_ID = "uniqueid";
@@ -171,6 +176,14 @@
                     .setWidthInPixels(WIDTH)
                     .setHeightInPixels(HEIGHT)
                     .build();
+    private static final VirtualNavigationTouchpadConfig NAVIGATION_TOUCHPAD_CONFIG =
+            new VirtualNavigationTouchpadConfig.Builder(
+                    /* touchpadHeight= */ HEIGHT, /* touchpadWidth= */ WIDTH)
+                    .setVendorId(VENDOR_ID)
+                    .setProductId(PRODUCT_ID)
+                    .setInputDeviceName(DEVICE_NAME)
+                    .setAssociatedDisplayId(DISPLAY_ID)
+                    .build();
 
     private Context mContext;
     private InputManagerMockHelper mInputManagerMockHelper;
@@ -301,22 +314,13 @@
                 mContext.getSystemService(WindowManager.class), threadVerifier);
         mSensorController = new SensorController(new Object(), VIRTUAL_DEVICE_ID);
 
-        mAssociationInfo = new AssociationInfo(1, 0, null,
+        mAssociationInfo = new AssociationInfo(/* associationId= */ 1, 0, null,
                 MacAddress.BROADCAST_ADDRESS, "", null, null, true, false, false, 0, 0);
 
         mVdms = new VirtualDeviceManagerService(mContext);
         mLocalService = mVdms.getLocalServiceInstance();
         mVdm = mVdms.new VirtualDeviceManagerImpl();
-
-        VirtualDeviceParams params = new VirtualDeviceParams
-                .Builder()
-                .setBlockedActivities(getBlockedActivities())
-                .build();
-        mDeviceImpl = new VirtualDeviceImpl(mContext,
-                mAssociationInfo, new Binder(), /* ownerUid= */ 0, VIRTUAL_DEVICE_ID,
-                mInputController, mSensorController, (int associationId) -> {},
-                mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params);
-        mVdms.addVirtualDevice(mDeviceImpl);
+        mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID, DEVICE_OWNER_UID_1);
     }
 
     @Test
@@ -380,7 +384,8 @@
                 .build();
         mDeviceImpl = new VirtualDeviceImpl(mContext,
                 mAssociationInfo, new Binder(), /* ownerUid */ 0, VIRTUAL_DEVICE_ID,
-                mInputController, mSensorController, (int associationId) -> {},
+                mInputController, mSensorController,
+                /* onDeviceCloseListener= */ (int deviceId) -> {},
                 mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params);
         mVdms.addVirtualDevice(mDeviceImpl);
 
@@ -389,6 +394,106 @@
     }
 
     @Test
+    public void getDeviceOwnerUid_oneDevice_returnsCorrectId() {
+        int ownerUid = mLocalService.getDeviceOwnerUid(mDeviceImpl.getDeviceId());
+        assertThat(ownerUid).isEqualTo(mDeviceImpl.getOwnerUid());
+    }
+
+    @Test
+    public void getDeviceOwnerUid_twoDevices_returnsCorrectId() {
+        int firstDeviceId = mDeviceImpl.getDeviceId();
+        int secondDeviceId = VIRTUAL_DEVICE_ID + 1;
+
+        createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2);
+
+        int secondDeviceOwner = mLocalService.getDeviceOwnerUid(secondDeviceId);
+        assertThat(secondDeviceOwner).isEqualTo(DEVICE_OWNER_UID_2);
+
+        int firstDeviceOwner = mLocalService.getDeviceOwnerUid(firstDeviceId);
+        assertThat(firstDeviceOwner).isEqualTo(DEVICE_OWNER_UID_1);
+    }
+
+    @Test
+    public void getDeviceOwnerUid_nonExistentDevice_returnsInvalidUid() {
+        int nonExistentDeviceId = DEVICE_ID_DEFAULT;
+        int ownerUid = mLocalService.getDeviceOwnerUid(nonExistentDeviceId);
+        assertThat(ownerUid).isEqualTo(Process.INVALID_UID);
+    }
+
+    @Test
+    public void getDeviceIdsForUid_noRunningApps_returnsNull() {
+        Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+        assertThat(deviceIds).isEmpty();
+    }
+
+    @Test
+    public void getDeviceIdsForUid_differentUidOnDevice_returnsNull() {
+        GenericWindowPolicyController gwpc =
+                mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+        mDeviceImpl.onVirtualDisplayCreatedLocked(gwpc, DISPLAY_ID);
+        gwpc.onRunningAppsChanged(Sets.newArraySet(UID_2));
+
+        Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+        assertThat(deviceIds).isEmpty();
+    }
+
+    @Test
+    public void getDeviceIdsForUid_oneUidOnDevice_returnsCorrectId() {
+        GenericWindowPolicyController gwpc =
+                mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+        mDeviceImpl.onVirtualDisplayCreatedLocked(gwpc, DISPLAY_ID);
+        gwpc.onRunningAppsChanged(Sets.newArraySet(UID_1));
+
+        Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+        assertThat(deviceIds).containsExactly(mDeviceImpl.getDeviceId());
+    }
+
+    @Test
+    public void getDeviceIdsForUid_twoUidsOnDevice_returnsCorrectId() {
+        GenericWindowPolicyController gwpc =
+                mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+        mDeviceImpl.onVirtualDisplayCreatedLocked(gwpc, DISPLAY_ID);
+        gwpc.onRunningAppsChanged(Sets.newArraySet(UID_1, UID_2));
+
+        Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+        assertThat(deviceIds).containsExactly(mDeviceImpl.getDeviceId());
+    }
+
+    @Test
+    public void getDeviceIdsForUid_twoDevicesUidOnOne_returnsCorrectId() {
+        int secondDeviceId = VIRTUAL_DEVICE_ID + 1;
+
+        VirtualDeviceImpl secondDevice = createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2);
+
+        GenericWindowPolicyController gwpc =
+                secondDevice.createWindowPolicyController(new ArrayList<>());
+        secondDevice.onVirtualDisplayCreatedLocked(gwpc, DISPLAY_ID_2);
+        gwpc.onRunningAppsChanged(Sets.newArraySet(UID_1));
+
+        Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+        assertThat(deviceIds).containsExactly(secondDevice.getDeviceId());
+    }
+
+    @Test
+    public void getDeviceIdsForUid_twoDevicesUidOnBoth_returnsCorrectId() {
+        int secondDeviceId = VIRTUAL_DEVICE_ID + 1;
+
+        VirtualDeviceImpl secondDevice = createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2);
+        GenericWindowPolicyController gwpc1 =
+                mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+        GenericWindowPolicyController gwpc2 =
+                mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+        mDeviceImpl.onVirtualDisplayCreatedLocked(gwpc1, DISPLAY_ID);
+        secondDevice.onVirtualDisplayCreatedLocked(gwpc2, DISPLAY_ID_2);
+        gwpc1.onRunningAppsChanged(Sets.newArraySet(UID_1));
+        gwpc2.onRunningAppsChanged(Sets.newArraySet(UID_1, UID_2));
+
+        Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+        assertThat(deviceIds).containsExactly(
+                mDeviceImpl.getDeviceId(), secondDevice.getDeviceId());
+    }
+
+    @Test
     public void onVirtualDisplayRemovedLocked_doesNotThrowException() {
         mDeviceImpl.onVirtualDisplayCreatedLocked(
                 mDeviceImpl.createWindowPolicyController(new ArrayList<>()), DISPLAY_ID);
@@ -423,7 +528,7 @@
         ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2));
         mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener);
 
-        mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_1, uids);
+        mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), uids);
         TestableLooper.get(this).processAllMessages();
 
         verify(mAppsOnVirtualDeviceListener).onAppsOnAnyVirtualDeviceChanged(uids);
@@ -436,13 +541,13 @@
         mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener);
 
         // Notifies that the running apps on the first virtual device has changed.
-        mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_1, uidsOnDevice1);
+        mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), uidsOnDevice1);
         TestableLooper.get(this).processAllMessages();
         verify(mAppsOnVirtualDeviceListener).onAppsOnAnyVirtualDeviceChanged(
                 new ArraySet<>(Arrays.asList(UID_1, UID_2)));
 
         // Notifies that the running apps on the second virtual device has changed.
-        mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_2, uidsOnDevice2);
+        mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId() + 1, uidsOnDevice2);
         TestableLooper.get(this).processAllMessages();
         // The union of the apps running on both virtual devices are sent to the listeners.
         verify(mAppsOnVirtualDeviceListener).onAppsOnAnyVirtualDeviceChanged(
@@ -450,7 +555,7 @@
 
         // Notifies that the running apps on the first virtual device has changed again.
         uidsOnDevice1.remove(UID_2);
-        mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_1, uidsOnDevice1);
+        mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), uidsOnDevice1);
         mLocalService.onAppsOnVirtualDeviceChanged();
         TestableLooper.get(this).processAllMessages();
         // The union of the apps running on both virtual devices are sent to the listeners.
@@ -459,7 +564,7 @@
 
         // Notifies that the running apps on the first virtual device has changed but with the same
         // set of UIDs.
-        mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_1, uidsOnDevice1);
+        mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), uidsOnDevice1);
         mLocalService.onAppsOnVirtualDeviceChanged();
         TestableLooper.get(this).processAllMessages();
         // Listeners should not be notified.
@@ -604,8 +709,67 @@
                         .build();
         mDeviceImpl.createVirtualTouchscreen(positiveConfig, BINDER);
         assertWithMessage(
-                "Virtual touchscreen should create input device descriptor on successful creation"
-                        + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty();
+            "Virtual touchscreen should create input device descriptor on successful creation"
+                + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty();
+    }
+
+    @Test
+    public void createVirtualNavigationTouchpad_noDisplay_failsSecurityException() {
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+                        BINDER));
+    }
+
+    @Test
+    public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        assertThrows(IllegalArgumentException.class,
+                () -> {
+                final VirtualNavigationTouchpadConfig zeroConfig =
+                        new VirtualNavigationTouchpadConfig.Builder(
+                                /* touchpadHeight= */ 0, /* touchpadWidth= */ 0)
+                                .setVendorId(VENDOR_ID)
+                                .setProductId(PRODUCT_ID)
+                                .setInputDeviceName(DEVICE_NAME)
+                                .setAssociatedDisplayId(DISPLAY_ID)
+                                .build();
+                mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER);
+            });
+    }
+
+    @Test
+    public void createVirtualNavigationTouchpad_negativeDisplayDimension_failsWithException() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        assertThrows(IllegalArgumentException.class,
+                () -> {
+                    final VirtualNavigationTouchpadConfig zeroConfig =
+                            new VirtualNavigationTouchpadConfig.Builder(
+                                    /* touchpadHeight= */ -50, /* touchpadWidth= */ 50)
+                                    .setVendorId(VENDOR_ID)
+                                    .setProductId(PRODUCT_ID)
+                                    .setInputDeviceName(DEVICE_NAME)
+                                    .setAssociatedDisplayId(DISPLAY_ID)
+                                    .build();
+                mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER);
+            });
+    }
+
+    @Test
+    public void createVirtualNavigationTouchpad_positiveDisplayDimension_successful() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        VirtualNavigationTouchpadConfig positiveConfig =
+                new VirtualNavigationTouchpadConfig.Builder(
+                            /* touchpadHeight= */ 50, /* touchpadWidth= */ 50)
+                        .setVendorId(VENDOR_ID)
+                        .setProductId(PRODUCT_ID)
+                        .setInputDeviceName(DEVICE_NAME)
+                        .setAssociatedDisplayId(DISPLAY_ID)
+                        .build();
+        mDeviceImpl.createVirtualNavigationTouchpad(positiveConfig, BINDER);
+        assertWithMessage(
+            "Virtual navigation touchpad should create input device descriptor on successful "
+            + "creation"
+                + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty();
     }
 
     @Test
@@ -652,6 +816,16 @@
     }
 
     @Test
+    public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
+                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+                        BINDER));
+    }
+
+    @Test
     public void createVirtualSensor_noPermission_failsSecurityException() {
         doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
                 eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
@@ -715,7 +889,18 @@
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
         mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER);
         assertWithMessage("Virtual touchscreen should register fd when the display matches").that(
-                mInputController.getInputDeviceDescriptors()).isNotEmpty();
+            mInputController.getInputDeviceDescriptors()).isNotEmpty();
+        verify(mNativeWrapperMock).openUinputTouchscreen(eq(DEVICE_NAME), eq(VENDOR_ID),
+                eq(PRODUCT_ID), anyString(), eq(HEIGHT), eq(WIDTH));
+    }
+
+    @Test
+    public void createVirtualNavigationTouchpad_hasDisplay_obtainFileDescriptor() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, BINDER);
+        assertWithMessage("Virtual navigation touchpad 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));
     }
@@ -1160,7 +1345,6 @@
                 /* targetDisplayCategory= */ null);
         verify(mContext).startActivityAsUser(argThat(intent ->
                 intent.filterEquals(blockedAppIntent)), any(), any());
-
     }
 
     @Test
@@ -1185,4 +1369,18 @@
         verify(mContext).startActivityAsUser(argThat(intent ->
                 intent.filterEquals(blockedAppIntent)), any(), any());
     }
+
+    private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid) {
+        VirtualDeviceParams params = new VirtualDeviceParams
+                .Builder()
+                .setBlockedActivities(getBlockedActivities())
+                .build();
+        VirtualDeviceImpl virtualDeviceImpl = new VirtualDeviceImpl(mContext,
+                mAssociationInfo, new Binder(), ownerUid, virtualDeviceId,
+                mInputController, mSensorController,
+                /* onDeviceCloseListener= */ (int deviceId) -> {},
+                mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params);
+        mVdms.addVirtualDevice(virtualDeviceImpl);
+        return virtualDeviceImpl;
+    }
 }
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 c7caa43..c6a0b0f 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -18,10 +18,14 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.DEFAULT_DISPLAY_GROUP;
+import static android.view.Display.TYPE_INTERNAL;
+import static android.view.Display.TYPE_VIRTUAL;
 
+import static com.android.server.display.DeviceStateToLayoutMap.STATE_DEFAULT;
 import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED;
 import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED;
 import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED;
+import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED;
 import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED;
 
@@ -69,7 +73,6 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Arrays;
-import java.util.Set;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -155,8 +158,8 @@
 
     @Test
     public void testDisplayDeviceAddAndRemove_Internal() {
-        DisplayDevice device = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
         // add
         LogicalDisplay displayAdded = add(device);
@@ -177,7 +180,7 @@
         testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_EXTERNAL);
         testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_WIFI);
         testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_OVERLAY);
-        testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_VIRTUAL);
+        testDisplayDeviceAddAndRemove_NonInternal(TYPE_VIRTUAL);
         testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_UNKNOWN);
 
         // Call the internal test again, just to verify that adding non-internal displays
@@ -187,9 +190,9 @@
 
     @Test
     public void testDisplayDeviceAdd_TwoInternalOneDefault() {
-        DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, 0);
-        DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800, 0);
+        DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
         LogicalDisplay display1 = add(device1);
         assertEquals(info(display1).address, info(device1).address);
@@ -202,10 +205,10 @@
 
     @Test
     public void testDisplayDeviceAdd_TwoInternalBothDefault() {
-        DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
-        DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
         LogicalDisplay display1 = add(device1);
         assertEquals(info(display1).address, info(device1).address);
@@ -220,7 +223,7 @@
     @Test
     public void testDisplayDeviceAddAndRemove_OneExternalDefault() {
         DisplayDevice device = createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
         // add
         LogicalDisplay displayAdded = add(device);
@@ -238,10 +241,10 @@
 
     @Test
     public void testDisplayDeviceAddAndRemove_SwitchDefault() {
-        DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
-        DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
         LogicalDisplay display1 = add(device1);
         assertEquals(info(display1).address, info(device1).address);
@@ -267,10 +270,10 @@
 
     @Test
     public void testGetDisplayIdsLocked() {
-        add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
+        add(createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
         add(createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, 0));
-        add(createDisplayDevice(Display.TYPE_VIRTUAL, 600, 800, 0));
+        add(createDisplayDevice(TYPE_VIRTUAL, 600, 800, 0));
 
         int [] ids = mLogicalDisplayMapper.getDisplayIdsLocked(Process.SYSTEM_UID,
                 /* includeDisabled= */ true);
@@ -280,71 +283,98 @@
     }
 
     @Test
-    public void testGetDisplayInfoForStateLocked_oneDisplayGroup_internalType() {
-        add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        add(createDisplayDevice(Display.TYPE_INTERNAL, 700, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
+    public void testGetDisplayInfoForStateLocked_defaultLayout() {
+        final DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        final DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 200, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
-        Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked(
-                DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP);
-        assertThat(displayInfos.size()).isEqualTo(1);
-        for (DisplayInfo displayInfo : displayInfos) {
-            assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY);
-            assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP);
-            assertThat(displayInfo.logicalWidth).isEqualTo(600);
-            assertThat(displayInfo.logicalHeight).isEqualTo(800);
-        }
+        add(device1);
+        add(device2);
+
+        Layout layout1 = new Layout();
+        layout1.createDisplayLocked(info(device1).address, /* isDefault= */ true,
+                /* isEnabled= */ true, mIdProducer);
+        layout1.createDisplayLocked(info(device2).address, /* isDefault= */ false,
+                /* isEnabled= */ true, mIdProducer);
+        when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1);
+        assertThat(layout1.size()).isEqualTo(2);
+        final int logicalId2 = layout1.getByAddress(info(device2).address).getLogicalDisplayId();
+
+        final DisplayInfo displayInfoDefault = mLogicalDisplayMapper.getDisplayInfoForStateLocked(
+                STATE_DEFAULT, DEFAULT_DISPLAY);
+        assertThat(displayInfoDefault.displayId).isEqualTo(DEFAULT_DISPLAY);
+        assertThat(displayInfoDefault.logicalWidth).isEqualTo(width(device1));
+        assertThat(displayInfoDefault.logicalHeight).isEqualTo(height(device1));
+
+        final DisplayInfo displayInfoOther = mLogicalDisplayMapper.getDisplayInfoForStateLocked(
+                STATE_DEFAULT, logicalId2);
+        assertThat(displayInfoOther).isNotNull();
+        assertThat(displayInfoOther.displayId).isEqualTo(logicalId2);
+        assertThat(displayInfoOther.logicalWidth).isEqualTo(width(device2));
+        assertThat(displayInfoOther.logicalHeight).isEqualTo(height(device2));
     }
 
     @Test
-    public void testGetDisplayInfoForStateLocked_oneDisplayGroup_differentTypes() {
-        add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        add(createDisplayDevice(Display.TYPE_EXTERNAL, 700, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
+    public void testGetDisplayInfoForStateLocked_multipleLayouts() {
+        final DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        final DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 200, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        final DisplayDevice device3 = createDisplayDevice(TYPE_VIRTUAL, 700, 800,
+                DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
 
-        Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked(
-                DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP);
-        assertThat(displayInfos.size()).isEqualTo(1);
-        for (DisplayInfo displayInfo : displayInfos) {
-            assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY);
-            assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP);
-            assertThat(displayInfo.logicalWidth).isEqualTo(600);
-            assertThat(displayInfo.logicalHeight).isEqualTo(800);
-        }
-    }
+        add(device1);
+        add(device2);
+        add(device3);
 
-    @Test
-    public void testGetDisplayInfoForStateLocked_multipleDisplayGroups_defaultGroup() {
-        add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        add(createDisplayDevice(Display.TYPE_VIRTUAL, 700, 800,
-                DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP));
+        Layout layout1 = new Layout();
+        layout1.createDisplayLocked(info(device1).address,
+                /* isDefault= */ true, /* isEnabled= */ true, mIdProducer);
+        when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1);
 
-        Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked(
-                DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP);
-        assertThat(displayInfos.size()).isEqualTo(1);
-        for (DisplayInfo displayInfo : displayInfos) {
-            assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY);
-            assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP);
-            assertThat(displayInfo.logicalWidth).isEqualTo(600);
-            assertThat(displayInfo.logicalHeight).isEqualTo(800);
-        }
+        final int layoutState2 = 2;
+        Layout layout2 = new Layout();
+        layout2.createDisplayLocked(info(device2).address,
+                /* isDefault= */ false, /* isEnabled= */ true, mIdProducer);
+        // Device3 is the default display.
+        layout2.createDisplayLocked(info(device3).address,
+                /* isDefault= */ true, /* isEnabled= */ true, mIdProducer);
+        when(mDeviceStateToLayoutMapSpy.get(layoutState2)).thenReturn(layout2);
+        assertThat(layout2.size()).isEqualTo(2);
+        final int logicalId2 = layout2.getByAddress(info(device2).address).getLogicalDisplayId();
+
+        // Default layout.
+        final DisplayInfo displayInfoLayout1Default =
+                mLogicalDisplayMapper.getDisplayInfoForStateLocked(
+                        STATE_DEFAULT, DEFAULT_DISPLAY);
+        assertThat(displayInfoLayout1Default.displayId).isEqualTo(DEFAULT_DISPLAY);
+        assertThat(displayInfoLayout1Default.logicalWidth).isEqualTo(width(device1));
+        assertThat(displayInfoLayout1Default.logicalHeight).isEqualTo(height(device1));
+
+        // Second layout, where device3 is the default display.
+        final DisplayInfo displayInfoLayout2Default =
+                mLogicalDisplayMapper.getDisplayInfoForStateLocked(
+                        layoutState2, DEFAULT_DISPLAY);
+        assertThat(displayInfoLayout2Default.displayId).isEqualTo(DEFAULT_DISPLAY);
+        assertThat(displayInfoLayout2Default.logicalWidth).isEqualTo(width(device3));
+        assertThat(displayInfoLayout2Default.logicalHeight).isEqualTo(height(device3));
+
+        final DisplayInfo displayInfoLayout2Other =
+                mLogicalDisplayMapper.getDisplayInfoForStateLocked(
+                        layoutState2, logicalId2);
+        assertThat(displayInfoLayout2Other).isNotNull();
+        assertThat(displayInfoLayout2Other.displayId).isEqualTo(logicalId2);
+        assertThat(displayInfoLayout2Other.logicalWidth).isEqualTo(width(device2));
+        assertThat(displayInfoLayout2Other.logicalHeight).isEqualTo(height(device2));
     }
 
     @Test
     public void testSingleDisplayGroup() {
-        LogicalDisplay display1 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        LogicalDisplay display2 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, 0));
-        LogicalDisplay display3 = add(createDisplayDevice(Display.TYPE_VIRTUAL, 600, 800, 0));
+        LogicalDisplay display1 = add(createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
+        LogicalDisplay display2 = add(createDisplayDevice(TYPE_INTERNAL, 600, 800, 0));
+        LogicalDisplay display3 = add(createDisplayDevice(TYPE_VIRTUAL, 600, 800, 0));
 
         assertEquals(DEFAULT_DISPLAY_GROUP,
                 mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display1)));
@@ -356,12 +386,12 @@
 
     @Test
     public void testMultipleDisplayGroups() {
-        LogicalDisplay display1 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
-        LogicalDisplay display2 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, 0));
+        LogicalDisplay display1 = add(createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
+        LogicalDisplay display2 = add(createDisplayDevice(TYPE_INTERNAL, 600, 800, 0));
 
 
-        TestDisplayDevice device3 = createDisplayDevice(Display.TYPE_VIRTUAL, 600, 800,
+        TestDisplayDevice device3 = createDisplayDevice(TYPE_VIRTUAL, 600, 800,
                 DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
         LogicalDisplay display3 = add(device3);
 
@@ -519,10 +549,10 @@
 
     @Test
     public void testDeviceStateLocked() {
-        DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
-        DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
         Layout layout = new Layout();
         layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
@@ -577,14 +607,11 @@
         DisplayAddress displayAddressThree = new TestUtils.TestDisplayAddress();
 
         TestDisplayDevice device1 = createDisplayDevice(displayAddressOne, "one",
-                Display.TYPE_INTERNAL, 600, 800,
-                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+                TYPE_INTERNAL, 600, 800, DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
         TestDisplayDevice device2 = createDisplayDevice(displayAddressTwo, "two",
-                Display.TYPE_INTERNAL, 200, 800,
-                DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+                TYPE_INTERNAL, 200, 800, DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
         TestDisplayDevice device3 = createDisplayDevice(displayAddressThree, "three",
-                Display.TYPE_INTERNAL, 600, 900,
-                DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+                TYPE_INTERNAL, 600, 900, DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
 
         Layout threeDevicesEnabledLayout = new Layout();
         threeDevicesEnabledLayout.createDisplayLocked(
@@ -603,7 +630,7 @@
                 /* isEnabled= */ true,
                 mIdProducer);
 
-        when(mDeviceStateToLayoutMapSpy.get(DeviceStateToLayoutMap.STATE_DEFAULT))
+        when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT))
                 .thenReturn(threeDevicesEnabledLayout);
 
         LogicalDisplay display1 = add(device1);
@@ -735,6 +762,14 @@
         return device.getDisplayDeviceInfoLocked();
     }
 
+    private int width(DisplayDevice device) {
+        return info(device).width;
+    }
+
+    private int height(DisplayDevice device) {
+        return info(device).height;
+    }
+
     private DisplayInfo info(LogicalDisplay display) {
         return display.getDisplayInfoLocked();
     }
diff --git a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
index e390bcc..3326f80f 100644
--- a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.input
 
+
 import android.content.Context
 import android.content.ContextWrapper
 import android.hardware.display.DisplayViewport
@@ -25,6 +26,7 @@
 import android.view.Display
 import android.view.PointerIcon
 import androidx.test.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import org.junit.Assert.assertFalse
@@ -287,6 +289,28 @@
         verify(native).setPointerAcceleration(eq(5f))
     }
 
+    @Test
+    fun setDeviceTypeAssociation_setsDeviceTypeAssociation() {
+        val inputPort = "inputPort"
+        val type = "type"
+
+        localService.setTypeAssociation(inputPort, type)
+
+        assertThat(service.getDeviceTypeAssociations()).asList().containsExactly(inputPort, type)
+            .inOrder()
+    }
+
+    @Test
+    fun setAndUnsetDeviceTypeAssociation_deviceTypeAssociationIsMissing() {
+        val inputPort = "inputPort"
+        val type = "type"
+
+        localService.setTypeAssociation(inputPort, type)
+        localService.unsetTypeAssociation(inputPort)
+
+        assertTrue(service.getDeviceTypeAssociations().isEmpty())
+    }
+
     private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) {
         val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) }
         thread.start()
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 4668e5d..5ca695b 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -85,7 +85,8 @@
     protected static final int SECONDARY_USER_ID = 20;
 
     private static final UserInfo PRIMARY_USER_INFO = new UserInfo(PRIMARY_USER_ID, null, null,
-            UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY);
+            UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY
+                    | UserInfo.FLAG_MAIN);
     private static final UserInfo SECONDARY_USER_INFO = new UserInfo(SECONDARY_USER_ID, null, null,
             UserInfo.FLAG_INITIALIZED);
 
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 196226a..41e3a08 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -59,65 +59,65 @@
 @Presubmit
 @RunWith(AndroidJUnit4.class)
 public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
+
     @Before
-    public void disableProcessCaches() {
+    public void setUp() {
         PropertyInvalidatedCache.disableForTestMode();
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+        mService.initializeSyntheticPassword(MANAGED_PROFILE_USER_ID);
     }
 
     @Test
-    public void testCreatePasswordPrimaryUser() throws RemoteException {
-        testCreateCredential(PRIMARY_USER_ID, newPassword("password"));
+    public void testSetPasswordPrimaryUser() throws RemoteException {
+        setAndVerifyCredential(PRIMARY_USER_ID, newPassword("password"));
     }
 
     @Test
-    public void testCreatePasswordFailsWithoutLockScreen() throws RemoteException {
-        testCreateCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, newPassword("password"));
+    public void testSetPasswordFailsWithoutLockScreen() throws RemoteException {
+        testSetCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, newPassword("password"));
     }
 
     @Test
-    public void testCreatePatternPrimaryUser() throws RemoteException {
-        testCreateCredential(PRIMARY_USER_ID, newPattern("123456789"));
+    public void testSetPatternPrimaryUser() throws RemoteException {
+        setAndVerifyCredential(PRIMARY_USER_ID, newPattern("123456789"));
     }
 
     @Test
-    public void testCreatePatternFailsWithoutLockScreen() throws RemoteException {
-        testCreateCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, newPattern("123456789"));
+    public void testSetPatternFailsWithoutLockScreen() throws RemoteException {
+        testSetCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, newPattern("123456789"));
     }
 
     @Test
     public void testChangePasswordPrimaryUser() throws RemoteException {
-        testChangeCredentials(PRIMARY_USER_ID, newPattern("78963214"), newPassword("asdfghjk"));
+        testChangeCredential(PRIMARY_USER_ID, newPattern("78963214"), newPassword("asdfghjk"));
     }
 
     @Test
     public void testChangePatternPrimaryUser() throws RemoteException {
-        testChangeCredentials(PRIMARY_USER_ID, newPassword("!£$%^&*(())"), newPattern("1596321"));
+        testChangeCredential(PRIMARY_USER_ID, newPassword("!£$%^&*(())"), newPattern("1596321"));
     }
 
     @Test
     public void testChangePasswordFailPrimaryUser() throws RemoteException {
-        initializeStorageWithCredential(PRIMARY_USER_ID, newPassword("password"));
-
+        setCredential(PRIMARY_USER_ID, newPassword("password"));
         assertFalse(mService.setLockCredential(newPassword("newpwd"), newPassword("badpwd"),
                     PRIMARY_USER_ID));
-        assertVerifyCredentials(PRIMARY_USER_ID, newPassword("password"));
+        assertVerifyCredential(PRIMARY_USER_ID, newPassword("password"));
     }
 
     @Test
     public void testClearPasswordPrimaryUser() throws RemoteException {
-        initializeStorageWithCredential(PRIMARY_USER_ID, newPassword("password"));
-        assertTrue(mService.setLockCredential(nonePassword(), newPassword("password"),
-                PRIMARY_USER_ID));
-        assertEquals(CREDENTIAL_TYPE_NONE, mService.getCredentialType(PRIMARY_USER_ID));
-        assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
+        setCredential(PRIMARY_USER_ID, newPassword("password"));
+        clearCredential(PRIMARY_USER_ID, newPassword("password"));
     }
 
     @Test
     public void testManagedProfileUnifiedChallenge() throws RemoteException {
+        mService.initializeSyntheticPassword(TURNED_OFF_PROFILE_USER_ID);
+
         final LockscreenCredential firstUnifiedPassword = newPassword("pwd-1");
         final LockscreenCredential secondUnifiedPassword = newPassword("pwd-2");
-        assertTrue(mService.setLockCredential(firstUnifiedPassword,
-                nonePassword(), PRIMARY_USER_ID));
+        setCredential(PRIMARY_USER_ID, firstUnifiedPassword);
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
         final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
@@ -146,14 +146,12 @@
         assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
 
         // Change primary password and verify that profile SID remains
-        assertTrue(mService.setLockCredential(
-                secondUnifiedPassword, firstUnifiedPassword, PRIMARY_USER_ID));
+        setCredential(PRIMARY_USER_ID, secondUnifiedPassword, firstUnifiedPassword);
         assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
         assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
 
         // Clear unified challenge
-        assertTrue(mService.setLockCredential(nonePassword(),
-                secondUnifiedPassword, PRIMARY_USER_ID));
+        clearCredential(PRIMARY_USER_ID, secondUnifiedPassword);
         assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
         assertEquals(0, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
         assertEquals(0, mGateKeeperService.getSecureUserId(TURNED_OFF_PROFILE_USER_ID));
@@ -163,12 +161,8 @@
     public void testManagedProfileSeparateChallenge() throws RemoteException {
         final LockscreenCredential primaryPassword = newPassword("primary");
         final LockscreenCredential profilePassword = newPassword("profile");
-        assertTrue(mService.setLockCredential(primaryPassword,
-                nonePassword(),
-                PRIMARY_USER_ID));
-        assertTrue(mService.setLockCredential(profilePassword,
-                nonePassword(),
-                MANAGED_PROFILE_USER_ID));
+        setCredential(PRIMARY_USER_ID, primaryPassword);
+        setCredential(MANAGED_PROFILE_USER_ID, profilePassword);
 
         final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
@@ -191,8 +185,7 @@
         assertNotNull(mGateKeeperService.getAuthToken(MANAGED_PROFILE_USER_ID));
         assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
 
-        assertTrue(mService.setLockCredential(
-                newPassword("pwd"), primaryPassword, PRIMARY_USER_ID));
+        setCredential(PRIMARY_USER_ID, newPassword("pwd"), primaryPassword);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 profilePassword, MANAGED_PROFILE_USER_ID, 0 /* flags */)
                 .getResponseCode());
@@ -207,8 +200,7 @@
         assertEquals(CREDENTIAL_TYPE_NONE, mService.getCredentialType(MANAGED_PROFILE_USER_ID));
 
         // Set a separate challenge on the profile
-        assertTrue(mService.setLockCredential(
-                newPassword("12345678"), nonePassword(), MANAGED_PROFILE_USER_ID));
+        setCredential(MANAGED_PROFILE_USER_ID, newPassword("12345678"));
         assertNotEquals(0, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
         assertEquals(CREDENTIAL_TYPE_PASSWORD, mService.getCredentialType(MANAGED_PROFILE_USER_ID));
 
@@ -221,11 +213,7 @@
 
     @Test
     public void testSetLockCredential_forPrimaryUser_sendsCredentials() throws Exception {
-        assertTrue(mService.setLockCredential(
-                newPassword("password"),
-                nonePassword(),
-                PRIMARY_USER_ID));
-
+        setCredential(PRIMARY_USER_ID, newPassword("password"));
         verify(mRecoverableKeyStoreManager)
                 .lockScreenSecretChanged(CREDENTIAL_TYPE_PASSWORD, "password".getBytes(),
                         PRIMARY_USER_ID);
@@ -234,11 +222,7 @@
     @Test
     public void testSetLockCredential_forProfileWithSeparateChallenge_sendsCredentials()
             throws Exception {
-        assertTrue(mService.setLockCredential(
-                newPattern("12345"),
-                nonePassword(),
-                MANAGED_PROFILE_USER_ID));
-
+        setCredential(MANAGED_PROFILE_USER_ID, newPattern("12345"));
         verify(mRecoverableKeyStoreManager)
                 .lockScreenSecretChanged(CREDENTIAL_TYPE_PATTERN, "12345".getBytes(),
                         MANAGED_PROFILE_USER_ID);
@@ -248,13 +232,8 @@
     public void testSetLockCredential_forProfileWithSeparateChallenge_updatesCredentials()
             throws Exception {
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, true, null);
-        initializeStorageWithCredential(MANAGED_PROFILE_USER_ID, newPattern("12345"));
-
-        assertTrue(mService.setLockCredential(
-                newPassword("newPassword"),
-                newPattern("12345"),
-                MANAGED_PROFILE_USER_ID));
-
+        setCredential(MANAGED_PROFILE_USER_ID, newPattern("12345"));
+        setCredential(MANAGED_PROFILE_USER_ID, newPassword("newPassword"), newPattern("12345"));
         verify(mRecoverableKeyStoreManager)
                 .lockScreenSecretChanged(CREDENTIAL_TYPE_PASSWORD, "newPassword".getBytes(),
                         MANAGED_PROFILE_USER_ID);
@@ -264,12 +243,7 @@
     public void testSetLockCredential_forProfileWithUnifiedChallenge_doesNotSendRandomCredential()
             throws Exception {
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
-
-        assertTrue(mService.setLockCredential(
-                newPattern("12345"),
-                nonePassword(),
-                PRIMARY_USER_ID));
-
+        setCredential(PRIMARY_USER_ID, newPattern("12345"));
         verify(mRecoverableKeyStoreManager, never())
                 .lockScreenSecretChanged(
                         eq(CREDENTIAL_TYPE_PASSWORD), any(), eq(MANAGED_PROFILE_USER_ID));
@@ -281,13 +255,9 @@
                     throws Exception {
         final LockscreenCredential oldCredential = newPassword("oldPassword");
         final LockscreenCredential newCredential = newPassword("newPassword");
-        initializeStorageWithCredential(PRIMARY_USER_ID, oldCredential);
+        setCredential(PRIMARY_USER_ID, oldCredential);
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
-
-        assertTrue(mService.setLockCredential(
-                newCredential,
-                oldCredential,
-                PRIMARY_USER_ID));
+        setCredential(PRIMARY_USER_ID, newCredential, oldCredential);
 
         verify(mRecoverableKeyStoreManager)
                 .lockScreenSecretChanged(CREDENTIAL_TYPE_PASSWORD, newCredential.getCredential(),
@@ -301,13 +271,9 @@
     public void
             testSetLockCredential_forPrimaryUserWithUnifiedChallengeProfile_removesBothCredentials()
                     throws Exception {
-        initializeStorageWithCredential(PRIMARY_USER_ID, newPassword("oldPassword"));
+        setCredential(PRIMARY_USER_ID, newPassword("oldPassword"));
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
-
-        assertTrue(mService.setLockCredential(
-                nonePassword(),
-                newPassword("oldPassword"),
-                PRIMARY_USER_ID));
+        clearCredential(PRIMARY_USER_ID, newPassword("oldPassword"));
 
         verify(mRecoverableKeyStoreManager)
                 .lockScreenSecretChanged(CREDENTIAL_TYPE_NONE, null, PRIMARY_USER_ID);
@@ -316,11 +282,10 @@
     }
 
     @Test
-    public void testSetLockCredential_nullCredential_removeBiometrics() throws RemoteException {
-        initializeStorageWithCredential(PRIMARY_USER_ID, newPattern("123654"));
+    public void testClearLockCredential_removesBiometrics() throws RemoteException {
+        setCredential(PRIMARY_USER_ID, newPattern("123654"));
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
-
-        mService.setLockCredential(nonePassword(), newPattern("123654"), PRIMARY_USER_ID);
+        clearCredential(PRIMARY_USER_ID, newPattern("123654"));
 
         // Verify fingerprint is removed
         verify(mFingerprintManager).removeAll(eq(PRIMARY_USER_ID), any());
@@ -335,14 +300,9 @@
             throws Exception {
         final LockscreenCredential parentPassword = newPassword("parentPassword");
         final LockscreenCredential profilePassword = newPassword("profilePassword");
-        initializeStorageWithCredential(PRIMARY_USER_ID, parentPassword);
+        setCredential(PRIMARY_USER_ID, parentPassword);
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
-
-        assertTrue(mService.setLockCredential(
-                profilePassword,
-                nonePassword(),
-                MANAGED_PROFILE_USER_ID));
-
+        setCredential(MANAGED_PROFILE_USER_ID, profilePassword);
         verify(mRecoverableKeyStoreManager)
                 .lockScreenSecretChanged(CREDENTIAL_TYPE_PASSWORD, profilePassword.getCredential(),
                         MANAGED_PROFILE_USER_ID);
@@ -356,9 +316,8 @@
         final LockscreenCredential profilePassword = newPattern("12345");
         mService.setSeparateProfileChallengeEnabled(
                 MANAGED_PROFILE_USER_ID, true, profilePassword);
-        initializeStorageWithCredential(PRIMARY_USER_ID, parentPassword);
-        // Create and verify separate profile credentials.
-        testCreateCredential(MANAGED_PROFILE_USER_ID, profilePassword);
+        setCredential(PRIMARY_USER_ID, parentPassword);
+        setAndVerifyCredential(MANAGED_PROFILE_USER_ID, profilePassword);
 
         mService.setSeparateProfileChallengeEnabled(
                 MANAGED_PROFILE_USER_ID, false, profilePassword);
@@ -372,7 +331,7 @@
     @Test
     public void testVerifyCredential_forPrimaryUser_sendsCredentials() throws Exception {
         final LockscreenCredential password = newPassword("password");
-        initializeStorageWithCredential(PRIMARY_USER_ID, password);
+        setCredential(PRIMARY_USER_ID, password);
         reset(mRecoverableKeyStoreManager);
 
         mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */);
@@ -386,10 +345,7 @@
     public void testVerifyCredential_forProfileWithSeparateChallenge_sendsCredentials()
             throws Exception {
         final LockscreenCredential pattern = newPattern("12345");
-        assertTrue(mService.setLockCredential(
-                pattern,
-                nonePassword(),
-                MANAGED_PROFILE_USER_ID));
+        setCredential(MANAGED_PROFILE_USER_ID, pattern);
         reset(mRecoverableKeyStoreManager);
 
         mService.verifyCredential(pattern, MANAGED_PROFILE_USER_ID, 0 /* flags */);
@@ -403,7 +359,7 @@
     public void verifyCredential_forPrimaryUserWithUnifiedChallengeProfile_sendsCredentialsForBoth()
                     throws Exception {
         final LockscreenCredential pattern = newPattern("12345");
-        initializeStorageWithCredential(PRIMARY_USER_ID, pattern);
+        setCredential(PRIMARY_USER_ID, pattern);
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
         reset(mRecoverableKeyStoreManager);
 
@@ -423,7 +379,7 @@
     }
 
     @Test
-    public void testCredentialChangeNotPossibleInSecureFrpModeDuringSuw() {
+    public void testSetCredentialNotPossibleInSecureFrpModeDuringSuw() {
         setUserSetupComplete(false);
         setSecureFrpMode(true);
         try {
@@ -433,21 +389,17 @@
     }
 
     @Test
-    public void testCredentialChangePossibleInSecureFrpModeAfterSuw() {
+    public void testSetCredentialPossibleInSecureFrpModeAfterSuw() throws RemoteException {
         setUserSetupComplete(true);
         setSecureFrpMode(true);
-        assertTrue(mService.setLockCredential(newPassword("1234"), nonePassword(),
-                PRIMARY_USER_ID));
+        setCredential(PRIMARY_USER_ID, newPassword("1234"));
     }
 
     @Test
     public void testPasswordHistoryDisabledByDefault() throws Exception {
         final int userId = PRIMARY_USER_ID;
         checkPasswordHistoryLength(userId, 0);
-        initializeStorageWithCredential(userId, nonePassword());
-        checkPasswordHistoryLength(userId, 0);
-
-        assertTrue(mService.setLockCredential(newPassword("1234"), nonePassword(), userId));
+        setCredential(userId, newPassword("1234"));
         checkPasswordHistoryLength(userId, 0);
     }
 
@@ -456,20 +408,18 @@
         final int userId = PRIMARY_USER_ID;
         when(mDevicePolicyManager.getPasswordHistoryLength(any(), eq(userId))).thenReturn(3);
         checkPasswordHistoryLength(userId, 0);
-        initializeStorageWithCredential(userId, nonePassword());
-        checkPasswordHistoryLength(userId, 0);
 
-        assertTrue(mService.setLockCredential(newPassword("pass1"), nonePassword(), userId));
+        setCredential(userId, newPassword("pass1"));
         checkPasswordHistoryLength(userId, 1);
 
-        assertTrue(mService.setLockCredential(newPassword("pass2"), newPassword("pass1"), userId));
+        setCredential(userId, newPassword("pass2"), newPassword("pass1"));
         checkPasswordHistoryLength(userId, 2);
 
-        assertTrue(mService.setLockCredential(newPassword("pass3"), newPassword("pass2"), userId));
+        setCredential(userId, newPassword("pass3"), newPassword("pass2"));
         checkPasswordHistoryLength(userId, 3);
 
         // maximum length should have been reached
-        assertTrue(mService.setLockCredential(newPassword("pass4"), newPassword("pass3"), userId));
+        setCredential(userId, newPassword("pass4"), newPassword("pass3"));
         checkPasswordHistoryLength(userId, 3);
     }
 
@@ -479,18 +429,11 @@
         assertEquals(expectedLen, hashes.length);
     }
 
-    private void testCreateCredential(int userId, LockscreenCredential credential)
-            throws RemoteException {
-        assertTrue(mService.setLockCredential(credential, nonePassword(), userId));
-        assertVerifyCredentials(userId, credential);
-    }
-
-    private void testCreateCredentialFailsWithoutLockScreen(
+    private void testSetCredentialFailsWithoutLockScreen(
             int userId, LockscreenCredential credential) throws RemoteException {
         mService.mHasSecureLockScreen = false;
-
         try {
-            mService.setLockCredential(credential, null, userId);
+            mService.setLockCredential(credential, nonePassword(), userId);
             fail("An exception should have been thrown.");
         } catch (UnsupportedOperationException e) {
             // Success - the exception was expected.
@@ -499,14 +442,14 @@
         assertEquals(CREDENTIAL_TYPE_NONE, mService.getCredentialType(userId));
     }
 
-    private void testChangeCredentials(int userId, LockscreenCredential newCredential,
+    private void testChangeCredential(int userId, LockscreenCredential newCredential,
             LockscreenCredential oldCredential) throws RemoteException {
-        initializeStorageWithCredential(userId, oldCredential);
-        assertTrue(mService.setLockCredential(newCredential, oldCredential, userId));
-        assertVerifyCredentials(userId, newCredential);
+        setCredential(userId, oldCredential);
+        setCredential(userId, newCredential, oldCredential);
+        assertVerifyCredential(userId, newCredential);
     }
 
-    private void assertVerifyCredentials(int userId, LockscreenCredential credential)
+    private void assertVerifyCredential(int userId, LockscreenCredential credential)
             throws RemoteException{
         VerifyCredentialResponse response = mService.verifyCredential(credential, userId,
                 0 /* flags */);
@@ -533,16 +476,29 @@
                 badCredential, userId, 0 /* flags */).getResponseCode());
     }
 
-    private void initializeStorageWithCredential(int userId, LockscreenCredential credential)
+    private void setAndVerifyCredential(int userId, LockscreenCredential newCredential)
             throws RemoteException {
-        assertEquals(0, mGateKeeperService.getSecureUserId(userId));
-        synchronized (mService.mSpManager) {
-            mService.initializeSyntheticPasswordLocked(userId);
-        }
-        if (credential.isNone()) {
+        setCredential(userId, newCredential);
+        assertVerifyCredential(userId, newCredential);
+    }
+
+    private void setCredential(int userId, LockscreenCredential newCredential)
+            throws RemoteException {
+        setCredential(userId, newCredential, nonePassword());
+    }
+
+    private void clearCredential(int userId, LockscreenCredential oldCredential)
+            throws RemoteException {
+        setCredential(userId, nonePassword(), oldCredential);
+    }
+
+    private void setCredential(int userId, LockscreenCredential newCredential,
+            LockscreenCredential oldCredential) throws RemoteException {
+        assertTrue(mService.setLockCredential(newCredential, oldCredential, userId));
+        assertEquals(newCredential.getType(), mService.getCredentialType(userId));
+        if (newCredential.isNone()) {
             assertEquals(0, mGateKeeperService.getSecureUserId(userId));
         } else {
-            assertTrue(mService.setLockCredential(credential, nonePassword(), userId));
             assertNotEquals(0, mGateKeeperService.getSecureUserId(userId));
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
index fc0ca7e..10869da 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
@@ -42,14 +42,13 @@
 public class LockscreenFrpTest extends BaseLockSettingsServiceTests {
 
     @Before
-    public void setDeviceNotProvisioned() throws Exception {
+    public void setUp() throws Exception {
+        PropertyInvalidatedCache.disableForTestMode();
+
         // FRP credential can only be verified prior to provisioning
         setDeviceProvisioned(false);
-    }
 
-    @Before
-    public void disableProcessCaches() {
-        PropertyInvalidatedCache.disableForTestMode();
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
     }
 
     @Test
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 b00467c..57593cf 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -19,7 +19,6 @@
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
-import static com.android.internal.widget.LockPatternUtils.CURRENT_LSKF_BASED_PROTECTOR_ID_KEY;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -59,9 +58,6 @@
 import org.mockito.ArgumentCaptor;
 
 import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-
 
 /**
  * atest FrameworksServicesTests:SyntheticPasswordTests
@@ -127,21 +123,11 @@
         return mGateKeeperService.getSecureUserId(SyntheticPasswordManager.fakeUserId(userId)) != 0;
     }
 
-    private boolean hasSyntheticPassword(int userId) throws RemoteException {
-        return mService.getLong(CURRENT_LSKF_BASED_PROTECTOR_ID_KEY, 0, userId) != 0;
-    }
-
-    private void initializeCredential(LockscreenCredential password, int userId)
+    private void initSpAndSetCredential(int userId, LockscreenCredential credential)
             throws RemoteException {
-        assertTrue(mService.setLockCredential(password, nonePassword(), userId));
-        assertEquals(CREDENTIAL_TYPE_PASSWORD, mService.getCredentialType(userId));
-        assertTrue(mService.isSyntheticPasswordBasedCredential(userId));
-    }
-
-    protected void initializeSyntheticPassword(int userId) {
-        synchronized (mService.mSpManager) {
-            mService.initializeSyntheticPasswordLocked(userId);
-        }
+        mService.initializeSyntheticPassword(userId);
+        assertTrue(mService.setLockCredential(credential, nonePassword(), userId));
+        assertEquals(credential.getType(), mService.getCredentialType(userId));
     }
 
     // Tests that the FRP credential is updated when an LSKF-based protector is created for the user
@@ -149,7 +135,7 @@
     @Test
     public void testFrpCredentialSyncedIfDeviceProvisioned() throws RemoteException {
         setDeviceProvisioned(true);
-        initializeSyntheticPassword(PRIMARY_USER_ID);
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
         verify(mStorage.mPersistentDataBlockManager).setFrpCredentialHandle(any());
     }
 
@@ -159,7 +145,7 @@
     @Test
     public void testEmptyFrpCredentialNotSyncedIfDeviceNotProvisioned() throws RemoteException {
         setDeviceProvisioned(false);
-        initializeSyntheticPassword(PRIMARY_USER_ID);
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
         verify(mStorage.mPersistentDataBlockManager, never()).setFrpCredentialHandle(any());
     }
 
@@ -169,7 +155,7 @@
     @Test
     public void testNonEmptyFrpCredentialSyncedIfDeviceNotProvisioned() throws RemoteException {
         setDeviceProvisioned(false);
-        initializeSyntheticPassword(PRIMARY_USER_ID);
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
         verify(mStorage.mPersistentDataBlockManager, never()).setFrpCredentialHandle(any());
         mService.setLockCredential(newPassword("password"), nonePassword(), PRIMARY_USER_ID);
         verify(mStorage.mPersistentDataBlockManager).setFrpCredentialHandle(any());
@@ -180,7 +166,7 @@
         final LockscreenCredential password = newPassword("password");
         final LockscreenCredential newPassword = newPassword("newpassword");
 
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         mService.setLockCredential(newPassword, password, PRIMARY_USER_ID);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
@@ -193,7 +179,7 @@
         LockscreenCredential password = newPassword("password");
         LockscreenCredential badPassword = newPassword("badpassword");
 
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
         verify(mActivityManager).unlockUser2(eq(PRIMARY_USER_ID), any());
@@ -207,7 +193,7 @@
         LockscreenCredential password = newPassword("password");
         LockscreenCredential badPassword = newPassword("newpassword");
 
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         // clear password
         mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID);
@@ -225,7 +211,7 @@
         LockscreenCredential password = newPassword("password");
         LockscreenCredential badPassword = newPassword("new");
 
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         mService.setLockCredential(badPassword, password, PRIMARY_USER_ID);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 badPassword, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
@@ -242,7 +228,7 @@
     public void testVerifyPassesPrimaryUserAuthSecret() throws RemoteException {
         LockscreenCredential password = newPassword("password");
 
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         reset(mAuthSecretService);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
@@ -253,7 +239,7 @@
     public void testSecondaryUserDoesNotPassAuthSecret() throws RemoteException {
         LockscreenCredential password = newPassword("password");
 
-        initializeCredential(password, SECONDARY_USER_ID);
+        initSpAndSetCredential(SECONDARY_USER_ID, password);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 password, SECONDARY_USER_ID, 0 /* flags */).getResponseCode());
         verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class));
@@ -262,7 +248,7 @@
     @Test
     public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecret() throws RemoteException {
         LockscreenCredential password = newPassword("password");
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID);
 
         reset(mAuthSecretService);
@@ -275,7 +261,7 @@
         LockscreenCredential password = newPassword("password");
         LockscreenCredential pattern = newPattern("123654");
         byte[] token = "some-high-entropy-secure-token".getBytes();
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         // Disregard any reportPasswordChanged() invocations as part of credential setup.
         flushHandlerTasks();
         reset(mDevicePolicyManager);
@@ -310,7 +296,7 @@
         LockscreenCredential password = newPassword("password");
         LockscreenCredential pattern = newPattern("123654");
         byte[] token = "some-high-entropy-secure-token".getBytes();
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
 
         long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
@@ -336,7 +322,7 @@
         LockscreenCredential pattern = newPattern("123654");
         LockscreenCredential newPassword = newPassword("password");
         byte[] token = "some-high-entropy-secure-token".getBytes();
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
 
         long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
@@ -356,38 +342,20 @@
     }
 
     @Test
-    public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNeedsMigration()
-            throws RemoteException {
+    public void testEscrowTokenActivatedImmediatelyIfNoUserPassword() throws RemoteException {
         final byte[] token = "some-high-entropy-secure-token".getBytes();
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
         long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
         assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
         assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
-        assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
-    }
-
-    @Test
-    public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNoMigration()
-            throws RemoteException {
-        final byte[] token = "some-high-entropy-secure-token".getBytes();
-        // By first setting a password and then clearing it, we enter the state where SP is
-        // initialized but the user currently has no password
-        initializeCredential(newPassword("password"), PRIMARY_USER_ID);
-        assertTrue(mService.setLockCredential(nonePassword(), newPassword("password"),
-                PRIMARY_USER_ID));
-        assertTrue(mService.isSyntheticPasswordBasedCredential(PRIMARY_USER_ID));
-
-        long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
-        assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
-        assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
-        assertTrue(hasSyntheticPassword(PRIMARY_USER_ID));
     }
 
     @Test
     public void testEscrowTokenActivatedLaterWithUserPassword() throws RemoteException {
         byte[] token = "some-high-entropy-secure-token".getBytes();
         LockscreenCredential password = newPassword("password");
-        mService.setLockCredential(password, nonePassword(), PRIMARY_USER_ID);
 
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
         // Token not activated immediately since user password exists
         assertFalse(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
@@ -407,6 +375,7 @@
         when(mUserManagerInternal.isUserManaged(PRIMARY_USER_ID)).thenReturn(false);
         when(mDeviceStateCache.isDeviceProvisioned()).thenReturn(true);
 
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
         try {
             mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
             fail("Escrow token should not be possible on unmanaged device");
@@ -421,7 +390,7 @@
 
         LockscreenCredential password = newPassword("password");
         LockscreenCredential pattern = newPattern("123654");
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
 
         long handle0 = mLocalService.addEscrowToken(token0, PRIMARY_USER_ID, null);
         long handle1 = mLocalService.addEscrowToken(token1, PRIMARY_USER_ID, null);
@@ -450,6 +419,7 @@
         byte[] token = "some-high-entropy-secure-token".getBytes();
 
         mService.mHasSecureLockScreen = false;
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
         long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
         assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
 
@@ -473,7 +443,7 @@
     @Test
     public void testGetHashFactorPrimaryUser() throws RemoteException {
         LockscreenCredential password = newPassword("password");
-        mService.setLockCredential(password, nonePassword(), PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         byte[] hashFactor = mService.getHashFactor(password, PRIMARY_USER_ID);
         assertNotNull(hashFactor);
 
@@ -486,6 +456,9 @@
 
     @Test
     public void testGetHashFactorManagedProfileUnifiedChallenge() throws RemoteException {
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+        mService.initializeSyntheticPassword(MANAGED_PROFILE_USER_ID);
+
         LockscreenCredential pattern = newPattern("1236");
         mService.setLockCredential(pattern, nonePassword(), PRIMARY_USER_ID);
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
@@ -494,6 +467,9 @@
 
     @Test
     public void testGetHashFactorManagedProfileSeparateChallenge() throws RemoteException {
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+        mService.initializeSyntheticPassword(MANAGED_PROFILE_USER_ID);
+
         LockscreenCredential primaryPassword = newPassword("primary");
         LockscreenCredential profilePassword = newPassword("profile");
         mService.setLockCredential(primaryPassword, nonePassword(), PRIMARY_USER_ID);
@@ -594,7 +570,7 @@
 
         LockscreenCredential password = newPassword("testGsiDisablesAuthSecret-password");
 
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
         verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class));
@@ -604,7 +580,7 @@
     public void testUnlockUserWithToken() throws Exception {
         LockscreenCredential password = newPassword("password");
         byte[] token = "some-high-entropy-secure-token".getBytes();
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         // Disregard any reportPasswordChanged() invocations as part of credential setup.
         flushHandlerTasks();
         reset(mDevicePolicyManager);
@@ -625,7 +601,7 @@
     @Test
     public void testPasswordChange_NoOrphanedFilesLeft() throws Exception {
         LockscreenCredential password = newPassword("password");
-        initializeCredential(password, PRIMARY_USER_ID);
+        initSpAndSetCredential(PRIMARY_USER_ID, password);
         assertTrue(mService.setLockCredential(password, password, PRIMARY_USER_ID));
         assertNoOrphanedFilesLeft(PRIMARY_USER_ID);
     }
@@ -633,6 +609,7 @@
     @Test
     public void testAddingEscrowToken_NoOrphanedFilesLeft() throws Exception {
         final byte[] token = "some-high-entropy-secure-token".getBytes();
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
         for (int i = 0; i < 16; i++) {
             long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null);
             assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java
index 51ddcef..2c9ba34 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java
@@ -40,6 +40,7 @@
 import com.android.internal.widget.LockscreenCredential;
 import com.android.internal.widget.VerifyCredentialResponse;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -49,6 +50,11 @@
 @RunWith(AndroidJUnit4.class)
 public class WeakEscrowTokenTests extends BaseLockSettingsServiceTests{
 
+    @Before
+    public void setUp() {
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+    }
+
     @Test
     public void testWeakTokenActivatedImmediatelyIfNoUserPassword()
             throws RemoteException {
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 6c13a6f..966c047 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
@@ -36,7 +36,7 @@
         assertEquals(Sets.newHashSet(), mPasswordSlotManager.getUsedSlots());
         mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, frpWeaverSlot, 0,
                 new byte[1]);
-        initializeSyntheticPassword(userId); // This should allocate a Weaver slot.
+        mService.initializeSyntheticPassword(userId); // This should allocate a Weaver slot.
         assertEquals(Sets.newHashSet(1), mPasswordSlotManager.getUsedSlots());
     }
 
@@ -52,7 +52,7 @@
         assertEquals(Sets.newHashSet(), mPasswordSlotManager.getUsedSlots());
         mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, frpWeaverSlot, 0,
                 new byte[1]);
-        initializeSyntheticPassword(userId); // This should allocate a Weaver slot.
+        mService.initializeSyntheticPassword(userId); // This should allocate a Weaver slot.
         assertEquals(Sets.newHashSet(0), mPasswordSlotManager.getUsedSlots());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
new file mode 100644
index 0000000..54fa272
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
@@ -0,0 +1,840 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+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.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageEvents.Event;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.UsageEventListener;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.InstallSourceInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ParceledListSlice;
+import android.os.FileUtils;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+import android.util.AtomicFile;
+import android.util.SparseSetArray;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.FieldSetter;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link com.android.server.pm.BackgroundInstallControlService}
+ */
+@Presubmit
+public final class BackgroundInstallControlServiceTest {
+    private static final String INSTALLER_NAME_1 = "installer1";
+    private static final String INSTALLER_NAME_2 = "installer2";
+    private static final String PACKAGE_NAME_1 = "package1";
+    private static final String PACKAGE_NAME_2 = "package2";
+    private static final String PACKAGE_NAME_3 = "package3";
+    private static final int USER_ID_1 = 1;
+    private static final int USER_ID_2 = 2;
+    private static final long USAGE_EVENT_TIMESTAMP_1 = 1000;
+    private static final long USAGE_EVENT_TIMESTAMP_2 = 2000;
+    private static final long USAGE_EVENT_TIMESTAMP_3 = 3000;
+    private static final long PACKAGE_ADD_TIMESTAMP_1 = 1500;
+
+    private BackgroundInstallControlService mBackgroundInstallControlService;
+    private PackageManagerInternal.PackageListObserver mPackageListObserver;
+    private UsageEventListener mUsageEventListener;
+    private TestLooper mTestLooper;
+    private Looper mLooper;
+    private File mFile;
+
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private IPackageManager mIPackageManager;
+    @Mock
+    private PackageManagerInternal mPackageManagerInternal;
+    @Mock
+    private UsageStatsManagerInternal mUsageStatsManagerInternal;
+    @Mock
+    private PermissionManagerServiceInternal mPermissionManager;
+    @Captor
+    private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor;
+    @Captor
+    private ArgumentCaptor<UsageEventListener> mUsageEventListenerCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mTestLooper = new TestLooper();
+        mLooper = mTestLooper.getLooper();
+        mFile = new File(
+                InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(),
+                "test");
+        mBackgroundInstallControlService = new BackgroundInstallControlService(
+                new MockInjector(mContext));
+
+        verify(mUsageStatsManagerInternal).registerListener(mUsageEventListenerCaptor.capture());
+        mUsageEventListener = mUsageEventListenerCaptor.getValue();
+
+        mBackgroundInstallControlService.onStart(true);
+        verify(mPackageManagerInternal).getPackageList(mPackageListObserverCaptor.capture());
+        mPackageListObserver = mPackageListObserverCaptor.getValue();
+    }
+
+    @After
+    public void tearDown() {
+        FileUtils.deleteContentsAndDir(mFile);
+    }
+
+    @Test
+    public void testInitBackgroundInstalledPackages_empty() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        assertNotNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        assertEquals(0,
+                mBackgroundInstallControlService.getBackgroundInstalledPackages().size());
+    }
+
+    @Test
+    public void testInitBackgroundInstalledPackages_one() {
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        FileOutputStream fileOutputStream;
+        try {
+            fileOutputStream = atomicFile.startWrite();
+        } catch (IOException e) {
+            fail("Failed to start write to states protobuf." + e);
+            return;
+        }
+
+        // Write test data to the file on the disk.
+        try {
+            ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
+            long token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
+            protoOutputStream.end(token);
+            protoOutputStream.flush();
+            atomicFile.finishWrite(fileOutputStream);
+        } catch (Exception e) {
+            fail("Failed to finish write to states protobuf. " + e);
+            atomicFile.failWrite(fileOutputStream);
+        }
+
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testInitBackgroundInstalledPackages_two() {
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        FileOutputStream fileOutputStream;
+        try {
+            fileOutputStream = atomicFile.startWrite();
+        } catch (IOException e) {
+            fail("Failed to start write to states protobuf." + e);
+            return;
+        }
+
+        // Write test data to the file on the disk.
+        try {
+            ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
+
+            long token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
+            protoOutputStream.end(token);
+
+            token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1);
+            protoOutputStream.end(token);
+
+            protoOutputStream.flush();
+            atomicFile.finishWrite(fileOutputStream);
+        } catch (Exception e) {
+            fail("Failed to finish write to states protobuf. " + e);
+            atomicFile.failWrite(fileOutputStream);
+        }
+
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(2, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2));
+    }
+
+    @Test
+    public void testWriteBackgroundInstalledPackagesToDisk_empty() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk();
+
+        // Read the file on the disk to verify
+        var packagesInDisk = new SparseSetArray<>();
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+            ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
+
+            while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (protoInputStream.getFieldNumber()
+                        != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
+                    continue;
+                }
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                String packageName = null;
+                int userId = UserHandle.USER_NULL;
+                while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (protoInputStream.getFieldNumber()) {
+                        case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            break;
+                        case (int) BackgroundInstalledPackageProto.USER_ID:
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            break;
+                        default:
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
+                    }
+                }
+                protoInputStream.end(token);
+                if (packageName != null && userId != UserHandle.USER_NULL) {
+                    packagesInDisk.add(userId, packageName);
+                } else {
+                    fail("Fails to get packageName or UserId from proto file");
+                }
+            }
+        } catch (IOException e) {
+            fail("Error reading state from the disk. " + e);
+        }
+
+        assertEquals(0, packagesInDisk.size());
+        assertEquals(packages.size(), packagesInDisk.size());
+    }
+
+    @Test
+    public void testWriteBackgroundInstalledPackagesToDisk_one() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+
+        packages.add(USER_ID_1, PACKAGE_NAME_1);
+        mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk();
+
+        // Read the file on the disk to verify
+        var packagesInDisk = new SparseSetArray<>();
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+            ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
+
+            while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (protoInputStream.getFieldNumber()
+                        != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
+                    continue;
+                }
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                String packageName = null;
+                int userId = UserHandle.USER_NULL;
+                while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (protoInputStream.getFieldNumber()) {
+                        case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            break;
+                        case (int) BackgroundInstalledPackageProto.USER_ID:
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            break;
+                        default:
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
+                    }
+                }
+                protoInputStream.end(token);
+                if (packageName != null && userId != UserHandle.USER_NULL) {
+                    packagesInDisk.add(userId, packageName);
+                } else {
+                    fail("Fails to get packageName or UserId from proto file");
+                }
+            }
+        } catch (IOException e) {
+            fail("Error reading state from the disk. " + e);
+        }
+
+        assertEquals(1, packagesInDisk.size());
+        assertEquals(packages.size(), packagesInDisk.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testWriteBackgroundInstalledPackagesToDisk_two() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+
+        packages.add(USER_ID_1, PACKAGE_NAME_1);
+        packages.add(USER_ID_2, PACKAGE_NAME_2);
+        mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk();
+
+        // Read the file on the disk to verify
+        var packagesInDisk = new SparseSetArray<>();
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+            ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
+
+            while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (protoInputStream.getFieldNumber()
+                        != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
+                    continue;
+                }
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                String packageName = null;
+                int userId = UserHandle.USER_NULL;
+                while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (protoInputStream.getFieldNumber()) {
+                        case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            break;
+                        case (int) BackgroundInstalledPackageProto.USER_ID:
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            break;
+                        default:
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
+                    }
+                }
+                protoInputStream.end(token);
+                if (packageName != null && userId != UserHandle.USER_NULL) {
+                    packagesInDisk.add(userId, packageName);
+                } else {
+                    fail("Fails to get packageName or UserId from proto file");
+                }
+            }
+        } catch (IOException e) {
+            fail("Error reading state from the disk. " + e);
+        }
+
+        assertEquals(2, packagesInDisk.size());
+        assertEquals(packages.size(), packagesInDisk.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2));
+    }
+
+    @Test
+    public void testHandleUsageEvent_permissionDenied() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, 0);
+        mTestLooper.dispatchAll();
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+    }
+
+    @Test
+    public void testHandleUsageEvent_permissionGranted() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, 0);
+        mTestLooper.dispatchAll();
+        assertEquals(1,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+    }
+
+    @Test
+    public void testHandleUsageEvent_ignoredEvent() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.USER_INTERACTION,
+                USER_ID_1, INSTALLER_NAME_1, 0);
+        mTestLooper.dispatchAll();
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+    }
+
+    @Test
+    public void testHandleUsageEvent_firstActivityResumedHalfTimeFrame() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        mTestLooper.dispatchAll();
+
+        var installerForegroundTimeFrames =
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames();
+        assertEquals(1, installerForegroundTimeFrames.numMaps());
+        assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1));
+
+        var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1);
+        assertEquals(1, foregroundTimeFrames.size());
+
+        var foregroundTimeFrame = foregroundTimeFrames.first();
+        assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame.startTimeStampMillis);
+        assertFalse(foregroundTimeFrame.isDone());
+    }
+
+    @Test
+    public void testHandleUsageEvent_firstActivityResumedOneTimeFrame() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        mTestLooper.dispatchAll();
+
+        var installerForegroundTimeFrames =
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames();
+        assertEquals(1, installerForegroundTimeFrames.numMaps());
+        assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1));
+
+        var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1);
+        assertEquals(1, foregroundTimeFrames.size());
+
+        var foregroundTimeFrame = foregroundTimeFrames.first();
+        assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame.startTimeStampMillis);
+        assertTrue(foregroundTimeFrame.isDone());
+    }
+
+    @Test
+    public void testHandleUsageEvent_firstActivityResumedOneAndHalfTimeFrame() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        mTestLooper.dispatchAll();
+
+        var installerForegroundTimeFrames =
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames();
+        assertEquals(1, installerForegroundTimeFrames.numMaps());
+        assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1));
+
+        var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1);
+        assertEquals(2, foregroundTimeFrames.size());
+
+        var foregroundTimeFrame1 = foregroundTimeFrames.first();
+        assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame1.startTimeStampMillis);
+        assertTrue(foregroundTimeFrame1.isDone());
+
+        var foregroundTimeFrame2 = foregroundTimeFrames.last();
+        assertEquals(USAGE_EVENT_TIMESTAMP_3, foregroundTimeFrame2.startTimeStampMillis);
+        assertFalse(foregroundTimeFrame2.isDone());
+    }
+
+    @Test
+    public void testHandleUsageEvent_firstNoneActivityResumed() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        mTestLooper.dispatchAll();
+
+        var installerForegroundTimeFrames =
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames();
+        assertEquals(1, installerForegroundTimeFrames.numMaps());
+        assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1));
+
+        var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1);
+        assertEquals(0, foregroundTimeFrames.size());
+    }
+
+    @Test
+    public void testHandleUsageEvent_packageAddedNoUsageEvent() throws
+            RemoteException, NoSuchFieldException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mIPackageManager.getApplicationInfo(
+                eq(PACKAGE_NAME_1),
+                eq(0L),
+                anyInt())
+        ).thenReturn(appInfo);
+
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                createTimestamp);
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testHandleUsageEvent_packageAddedInsideTimeFrame() throws
+            RemoteException, NoSuchFieldException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mIPackageManager.getApplicationInfo(
+                eq(PACKAGE_NAME_1),
+                eq(0L),
+                anyInt())
+        ).thenReturn(appInfo);
+
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                createTimestamp);
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        // The following 2 usage events generation is the only difference from the
+        // testHandleUsageEvent_packageAddedNoUsageEvent test.
+        // The 2 usage events make the package adding inside a time frame.
+        // So it's not a background install. Thus, it's null for the return of
+        // mBackgroundInstallControlService.getBackgroundInstalledPackages()
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+    }
+
+    @Test
+    public void testHandleUsageEvent_packageAddedOutsideTimeFrame1() throws
+            RemoteException, NoSuchFieldException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mIPackageManager.getApplicationInfo(
+                eq(PACKAGE_NAME_1),
+                eq(0L),
+                anyInt())
+        ).thenReturn(appInfo);
+
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                createTimestamp);
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        // The following 2 usage events generation is the only difference from the
+        // testHandleUsageEvent_packageAddedNoUsageEvent test.
+        // The 2 usage events make the package adding outside a time frame.
+        // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame,
+        // it's a background install. Thus, it's not null for the return of
+        // mBackgroundInstallControlService.getBackgroundInstalledPackages()
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+    @Test
+    public void testHandleUsageEvent_packageAddedOutsideTimeFrame2() throws
+            RemoteException, NoSuchFieldException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mIPackageManager.getApplicationInfo(
+                eq(PACKAGE_NAME_1),
+                eq(0L),
+                anyInt())
+        ).thenReturn(appInfo);
+
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                createTimestamp);
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        // The following 2 usage events generation is the only difference from the
+        // testHandleUsageEvent_packageAddedNoUsageEvent test.
+        // These 2 usage events are triggered by INSTALLER_NAME_2.
+        // The 2 usage events make the package adding outside a time frame.
+        // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame,
+        // it's a background install. Thus, it's not null for the return of
+        // mBackgroundInstallControlService.getBackgroundInstalledPackages()
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3);
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testPackageRemoved() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+
+        packages.add(USER_ID_1, PACKAGE_NAME_1);
+        packages.add(USER_ID_2, PACKAGE_NAME_2);
+
+        assertEquals(2, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2));
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        mPackageListObserver.onPackageRemoved(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        assertEquals(1, packages.size());
+        assertFalse(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2));
+    }
+
+    @Test
+    public void testGetBackgroundInstalledPackages() throws RemoteException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var bgPackages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(bgPackages);
+
+        bgPackages.add(USER_ID_1, PACKAGE_NAME_1);
+        bgPackages.add(USER_ID_2, PACKAGE_NAME_2);
+
+        assertEquals(2, bgPackages.size());
+        assertTrue(bgPackages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(bgPackages.contains(USER_ID_2, PACKAGE_NAME_2));
+
+        List<PackageInfo> packages = new ArrayList<>();
+        var packageInfo1 = makePackageInfo(PACKAGE_NAME_1);
+        packages.add(packageInfo1);
+        var packageInfo2 = makePackageInfo(PACKAGE_NAME_2);
+        packages.add(packageInfo2);
+        var packageInfo3 = makePackageInfo(PACKAGE_NAME_3);
+        packages.add(packageInfo3);
+        doReturn(new ParceledListSlice<>(packages)).when(mIPackageManager).getInstalledPackages(
+                anyLong(), anyInt());
+
+        var resultPackages =
+                mBackgroundInstallControlService.getBackgroundInstalledPackages(0L, USER_ID_1);
+        assertEquals(1, resultPackages.getList().size());
+        assertTrue(resultPackages.getList().contains(packageInfo1));
+        assertFalse(resultPackages.getList().contains(packageInfo2));
+        assertFalse(resultPackages.getList().contains(packageInfo3));
+    }
+
+    /**
+     * Mock a usage event occurring.
+     *
+     * @param usageEventId id of a usage event
+     * @param userId user id of a usage event
+     * @param pkgName package name of a usage event
+     * @param timestamp timestamp of a usage event
+     */
+    private void generateUsageEvent(int usageEventId,
+            int userId,
+            String pkgName,
+            long timestamp) {
+        Event event = new Event(usageEventId, timestamp);
+        event.mPackage = pkgName;
+        mUsageEventListener.onUsageEvent(userId, event);
+    }
+
+    private PackageInfo makePackageInfo(String packageName) {
+        PackageInfo pkg = new PackageInfo();
+        pkg.packageName = packageName;
+        pkg.applicationInfo = new ApplicationInfo();
+        return pkg;
+    }
+
+    private class MockInjector implements BackgroundInstallControlService.Injector {
+        private final Context mContext;
+
+        MockInjector(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public Context getContext() {
+            return mContext;
+        }
+
+        @Override
+        public IPackageManager getIPackageManager() {
+            return mIPackageManager;
+        }
+
+        @Override
+        public PackageManagerInternal getPackageManagerInternal() {
+            return mPackageManagerInternal;
+        }
+
+        @Override
+        public UsageStatsManagerInternal getUsageStatsManagerInternal() {
+            return mUsageStatsManagerInternal;
+        }
+
+        @Override
+        public PermissionManagerServiceInternal getPermissionManager() {
+            return mPermissionManager;
+        }
+
+        @Override
+        public Looper getLooper() {
+            return mLooper;
+        }
+
+        @Override
+        public File getDiskFile() {
+            return mFile;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java b/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java
index b034b0d..fe31b9c 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java
@@ -257,7 +257,6 @@
                 .build();
 
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ false,
                 /* useProximitySensor= */ false,
                 /* boostScreenBrightness= */ false,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -273,7 +272,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_DIM);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(false);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(false);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(false);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
@@ -297,7 +295,6 @@
         mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_DOZE);
 
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ true,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -313,7 +310,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_DOZE);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_ON);
@@ -336,7 +332,6 @@
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING);
 
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ true,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -352,7 +347,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
@@ -374,7 +368,6 @@
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
 
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ true,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -390,7 +383,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
@@ -412,7 +404,6 @@
         mPowerGroup.sleepLocked(TIMESTAMP1, UID, GO_TO_SLEEP_REASON_TIMEOUT);
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ true,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -428,7 +419,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
@@ -451,7 +441,6 @@
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
         mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_SCREEN_BRIGHT);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ true,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -467,7 +456,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
@@ -488,7 +476,6 @@
                 .build();
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ true,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -504,7 +491,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
@@ -526,7 +512,6 @@
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mPowerGroup.setUserActivitySummaryLocked(USER_ACTIVITY_SCREEN_BRIGHT);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ true,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -542,7 +527,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
@@ -563,7 +547,6 @@
                 .build();
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
-                /* autoBrightness = */ true,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -579,7 +562,6 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
-        assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
index 22e44f8..71c8c1d 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
@@ -93,43 +93,44 @@
         }
 
         final BatteryStatsHistoryIterator iterator =
-                mBatteryStats.createBatteryStatsHistoryIterator();
+                mBatteryStats.iterateBatteryStatsHistory();
 
-        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+        BatteryStats.HistoryItem item;
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_RESET, BatteryStats.HistoryItem.EVENT_NONE,
                 null, 0, 3_600_000, 90, 1_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
                 null, 0, 3_600_000, 90, 1_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
                 null, 0, 2_400_000, 80, 2_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
                 null, 0, 2_400_000, 80, 2_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE,
                 BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_START,
                 "foo", APP_UID, 2_400_000, 80, 3_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE,
                 BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_FINISH,
                 "foo", APP_UID, 2_400_000, 80, 3_001_000);
 
-        assertThat(iterator.next(item)).isFalse();
+        assertThat(iterator.hasNext()).isFalse();
+        assertThat(iterator.next()).isNull();
     }
 
     // Test history that spans multiple buffers and uses more than 32k different strings.
@@ -163,21 +164,21 @@
         }
 
         final BatteryStatsHistoryIterator iterator =
-                mBatteryStats.createBatteryStatsHistoryIterator();
+                mBatteryStats.iterateBatteryStatsHistory();
 
-        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
-        assertThat(iterator.next(item)).isTrue();
+        BatteryStats.HistoryItem item;
+        assertThat(item = iterator.next()).isNotNull();
         assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_RESET);
         assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
         assertThat(item.eventTag).isNull();
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_UPDATE);
         assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
         assertThat(item.eventTag).isNull();
         assertThat(item.time).isEqualTo(1_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_UPDATE);
         assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
         assertThat(item.eventTag).isNull();
@@ -186,7 +187,7 @@
         for (int i = 0; i < eventCount; i++) {
             String name = "a" + (i % 10);
             do {
-                assertThat(iterator.next(item)).isTrue();
+                assertThat(item = iterator.next()).isNotNull();
                 // Skip a blank event inserted at the start of every buffer
             } while (item.cmd != BatteryStats.HistoryItem.CMD_UPDATE
                     || item.eventCode == BatteryStats.HistoryItem.EVENT_NONE);
@@ -196,7 +197,7 @@
             assertThat(item.eventTag.string).isEqualTo(name);
 
             do {
-                assertThat(iterator.next(item)).isTrue();
+                assertThat(item = iterator.next()).isNotNull();
             } while (item.cmd != BatteryStats.HistoryItem.CMD_UPDATE
                     || item.eventCode == BatteryStats.HistoryItem.EVENT_NONE);
 
@@ -205,7 +206,8 @@
             assertThat(item.eventTag.string).isEqualTo(name);
         }
 
-        assertThat(iterator.next(item)).isFalse();
+        assertThat(iterator.hasNext()).isFalse();
+        assertThat(iterator.next()).isNull();
     }
 
     private void assertHistoryItem(BatteryStats.HistoryItem item, int command, int eventCode,
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index 3f5d331..22a7e8d 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -309,10 +309,10 @@
         mHistory.recordMeasuredEnergyDetails(200, 200, details);
 
         BatteryStatsHistoryIterator iterator = mHistory.iterate();
-        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
-        assertThat(iterator.next(item)).isTrue(); // First item contains current time only
+        BatteryStats.HistoryItem item;
+        assertThat(item = iterator.next()).isNotNull(); // First item contains current time only
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
 
         String dump = toString(item, /* checkin */ false);
         assertThat(dump).contains("+200ms");
@@ -344,9 +344,9 @@
 
         BatteryStatsHistoryIterator iterator = mHistory.iterate();
         BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
-        assertThat(iterator.next(item)).isTrue(); // First item contains current time only
+        assertThat(item = iterator.next()).isNotNull(); // First item contains current time only
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
 
         String dump = toString(item, /* checkin */ false);
         assertThat(dump).contains("+200ms");
@@ -361,7 +361,7 @@
         assertThat(checkin).contains("XB,3,2,HIGH");
         assertThat(checkin).contains("XC,10123,100,200,300");
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
 
         dump = toString(item, /* checkin */ false);
         assertThat(dump).contains("+300ms");
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsNoteTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
index 5b51868..773a2dc 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
@@ -83,6 +83,7 @@
  * Run: adb shell am instrument -e class BatteryStatsNoteTest -w \
  *      com.android.frameworks.coretests/androidx.test.runner.AndroidJUnitRunner
  */
+@SuppressWarnings("GuardedBy")
 public class BatteryStatsNoteTest extends TestCase {
     private static final String TAG = BatteryStatsNoteTest.class.getSimpleName();
 
@@ -271,11 +272,11 @@
         clocks.realtime = clocks.uptime = 220;
         bi.noteLongPartialWakelockFinish(name, historyName, ISOLATED_UID);
 
-        final BatteryStatsHistoryIterator iterator =  bi.createBatteryStatsHistoryIterator();
+        final BatteryStatsHistoryIterator iterator =  bi.iterateBatteryStatsHistory();
 
-        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+        BatteryStats.HistoryItem item;
 
-        while (iterator.next(item)) {
+        while ((item = iterator.next()) != null) {
             if (item.eventCode == HistoryItem.EVENT_LONG_WAKE_LOCK_START) break;
         }
         assertThat(item.eventCode).isEqualTo(HistoryItem.EVENT_LONG_WAKE_LOCK_START);
@@ -283,7 +284,7 @@
         assertThat(item.eventTag.string).isEqualTo(historyName);
         assertThat(item.eventTag.uid).isEqualTo(UID);
 
-        while (iterator.next(item)) {
+        while ((item = iterator.next()) != null) {
             if (item.eventCode == HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH) break;
         }
         assertThat(item.eventCode).isEqualTo(HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH);
@@ -327,11 +328,11 @@
         clocks.realtime = clocks.uptime = 220;
         bi.noteLongPartialWakelockFinish(name, historyName, ISOLATED_UID);
 
-        final BatteryStatsHistoryIterator iterator = bi.createBatteryStatsHistoryIterator();
+        final BatteryStatsHistoryIterator iterator = bi.iterateBatteryStatsHistory();
 
-        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+        BatteryStats.HistoryItem item;
 
-        while (iterator.next(item)) {
+        while ((item = iterator.next()) != null) {
             if (item.eventCode == HistoryItem.EVENT_LONG_WAKE_LOCK_START) break;
         }
         assertThat(item.eventCode).isEqualTo(HistoryItem.EVENT_LONG_WAKE_LOCK_START);
@@ -339,7 +340,7 @@
         assertThat(item.eventTag.string).isEqualTo(historyName);
         assertThat(item.eventTag.uid).isEqualTo(UID);
 
-        while (iterator.next(item)) {
+        while ((item = iterator.next()) != null) {
             if (item.eventCode == HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH) break;
         }
         assertThat(item.eventCode).isEqualTo(HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH);
@@ -941,26 +942,27 @@
         clocks.realtime = clocks.uptime = 5000;
         bi.noteAlarmFinishLocked("foo", null, UID);
 
-        HistoryItem item = new HistoryItem();
-        assertTrue(bi.startIteratingHistoryLocked());
+        BatteryStatsHistoryIterator iterator = bi.iterateBatteryStatsHistory();
+        HistoryItem item;
 
-        assertTrue(bi.getNextHistoryLocked(item));
+        assertThat(item = iterator.next()).isNotNull();
         assertEquals(HistoryItem.EVENT_ALARM_START, item.eventCode);
         assertEquals("foo", item.eventTag.string);
         assertEquals(UID, item.eventTag.uid);
 
         // TODO(narayan): Figure out why this event is written to the history buffer. See
         // test below where it is being interspersed between multiple START events too.
-        assertTrue(bi.getNextHistoryLocked(item));
+        assertThat(item = iterator.next()).isNotNull();
         assertEquals(HistoryItem.EVENT_NONE, item.eventCode);
 
-        assertTrue(bi.getNextHistoryLocked(item));
+        assertThat(item = iterator.next()).isNotNull();
         assertEquals(HistoryItem.EVENT_ALARM_FINISH, item.eventCode);
         assertTrue(item.isDeltaData());
         assertEquals("foo", item.eventTag.string);
         assertEquals(UID, item.eventTag.uid);
 
-        assertFalse(bi.getNextHistoryLocked(item));
+        assertThat(iterator.hasNext()).isFalse();
+        assertThat(iterator.next()).isNull();
     }
 
     @SmallTest
@@ -980,28 +982,28 @@
         clocks.realtime = clocks.uptime = 5000;
         bi.noteAlarmFinishLocked("foo", ws, UID);
 
-        HistoryItem item = new HistoryItem();
-        assertTrue(bi.startIteratingHistoryLocked());
+        BatteryStatsHistoryIterator iterator = bi.iterateBatteryStatsHistory();
+        HistoryItem item;
 
-        assertTrue(bi.getNextHistoryLocked(item));
+        assertThat(item = iterator.next()).isNotNull();
         assertEquals(HistoryItem.EVENT_ALARM_START, item.eventCode);
         assertEquals("foo", item.eventTag.string);
         assertEquals(100, item.eventTag.uid);
 
-        assertTrue(bi.getNextHistoryLocked(item));
+        assertThat(item = iterator.next()).isNotNull();
         assertEquals(HistoryItem.EVENT_NONE, item.eventCode);
 
-        assertTrue(bi.getNextHistoryLocked(item));
+        assertThat(item = iterator.next()).isNotNull();
         assertEquals(HistoryItem.EVENT_ALARM_START, item.eventCode);
         assertEquals("foo", item.eventTag.string);
         assertEquals(500, item.eventTag.uid);
 
-        assertTrue(bi.getNextHistoryLocked(item));
+        assertThat(item = iterator.next()).isNotNull();
         assertEquals(HistoryItem.EVENT_ALARM_FINISH, item.eventCode);
         assertEquals("foo", item.eventTag.string);
         assertEquals(100, item.eventTag.uid);
 
-        assertTrue(bi.getNextHistoryLocked(item));
+        assertThat(item = iterator.next()).isNotNull();
         assertEquals(HistoryItem.EVENT_ALARM_FINISH, item.eventCode);
         assertEquals("foo", item.eventTag.string);
         assertEquals(500, item.eventTag.uid);
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 9d46e11..968609b 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -219,36 +219,37 @@
 
         final BatteryStatsHistoryIterator iterator =
                 unparceled.iterateBatteryStatsHistory();
-        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+        BatteryStats.HistoryItem item;
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_RESET, BatteryStats.HistoryItem.EVENT_NONE,
                 null, 0, 3_600_000, 90, 1_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
                 null, 0, 3_600_000, 90, 1_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
                 null, 0, 3_600_000, 90, 2_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE,
                 BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_START,
                 "foo", APP_UID, 3_600_000, 90, 3_000_000);
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertHistoryItem(item,
                 BatteryStats.HistoryItem.CMD_UPDATE,
                 BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_FINISH,
                 "foo", APP_UID, 3_600_000, 90, 3_001_000);
 
-        assertThat(iterator.next(item)).isFalse();
+        assertThat(iterator.hasNext()).isFalse();
+        assertThat(iterator.next()).isNull();
     }
 
     @Test
@@ -304,16 +305,16 @@
                 BatteryUsageStats.class);
 
         BatteryStatsHistoryIterator iterator = unparceled.iterateBatteryStatsHistory();
-        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+        BatteryStats.HistoryItem item;
 
-        assertThat(iterator.next(item)).isTrue();
+        assertThat(item = iterator.next()).isNotNull();
         assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_RESET);
 
         int expectedUid = 1;
-        while (iterator.next(item)) {
+        while ((item = iterator.next()) != null) {
             while (item.cmd != BatteryStats.HistoryItem.CMD_UPDATE
                     || item.eventCode == BatteryStats.HistoryItem.EVENT_NONE) {
-                assertThat(iterator.next(item)).isTrue();
+                assertThat(item = iterator.next()).isNotNull();
             }
             int uid = item.eventTag.uid;
             assertThat(uid).isEqualTo(expectedUid++);
diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml
index 107bbe1..593ee4a 100644
--- a/services/tests/wmtests/AndroidManifest.xml
+++ b/services/tests/wmtests/AndroidManifest.xml
@@ -45,6 +45,7 @@
     <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION"/>
 
     <!-- TODO: Remove largeHeap hack when memory leak is fixed (b/123984854) -->
     <application android:debuggable="true"
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 d16e11b..43627f4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -1637,6 +1637,8 @@
         final ActivityRecord target = new ActivityBuilder(mAtm).setAffinity(info.taskAffinity)
                 .build();
         final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK, false);
+        spyOn(starter);
+        doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
         startActivityInner(starter, target, null /* source */, null /* options */,
                 null /* inTask */, null /* inTaskFragment */);
 
@@ -1661,6 +1663,8 @@
         final ActivityRecord target = new ActivityBuilder(mAtm).setRequiredDisplayCategory("auto")
                 .setAffinity(info.taskAffinity).build();
         final ActivityStarter starter = prepareStarter(0, false);
+        spyOn(starter);
+        doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
         startActivityInner(starter, target,  task.getBottomMostActivity(), null /* options */,
                 null /* inTask */, null /* inTaskFragment */);
 
@@ -1685,7 +1689,10 @@
         final ActivityRecord target = new ActivityBuilder(mAtm)
                 .setRequiredDisplayCategory(info.requiredDisplayCategory)
                 .setAffinity(info.taskAffinity).build();
+
         final ActivityStarter starter = prepareStarter(0, false);
+        spyOn(starter);
+        doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
         startActivityInner(starter, target,  task.getBottomMostActivity(), null /* options */,
                 null /* inTask */, null /* inTaskFragment */);
 
@@ -1706,6 +1713,8 @@
         inTask.inRecents = true;
 
         final ActivityStarter starter = prepareStarter(0, false);
+        spyOn(starter);
+        doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
         final ActivityRecord target = new ActivityBuilder(mAtm).build();
         startActivityInner(starter, target, null /* source */, null /* options */, inTask,
                 null /* inTaskFragment */);
@@ -1724,6 +1733,8 @@
         inTask.inRecents = true;
 
         final ActivityStarter starter = prepareStarter(0, false);
+        spyOn(starter);
+        doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
         final ActivityRecord target = new ActivityBuilder(mAtm).setRequiredDisplayCategory("auto")
                 .build();
         startActivityInner(starter, target, null /* source */, null /* options */, inTask,
diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
index 49fd1ab..92dd047 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
@@ -52,6 +52,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.wm.ContentRecorder.MediaProjectionManagerWrapper;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -79,7 +81,7 @@
     private ContentRecordingSession mTaskSession;
     private static Point sSurfaceSize;
     private ContentRecorder mContentRecorder;
-    @Mock private ContentRecorder.MediaProjectionManagerWrapper mMediaProjectionManagerWrapper;
+    @Mock private MediaProjectionManagerWrapper mMediaProjectionManagerWrapper;
     private SurfaceControl mRecordedSurface;
     // Handle feature flag.
     private ConfigListener mConfigListener;
@@ -241,7 +243,7 @@
     }
 
     @Test
-    public void testOnTaskConfigurationChanged_resizesSurface() {
+    public void testOnTaskOrientationConfigurationChanged_resizesSurface() {
         mContentRecorder.setContentRecordingSession(mTaskSession);
         mContentRecorder.updateRecording();
 
@@ -256,6 +258,29 @@
     }
 
     @Test
+    public void testOnTaskBoundsConfigurationChanged_notifiesCallback() {
+        final int recordedWidth = 333;
+        final int recordedHeight = 999;
+        // WHEN a recording is ongoing.
+        mContentRecorder.setContentRecordingSession(mTaskSession);
+        mContentRecorder.updateRecording();
+        assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
+
+        // WHEN a configuration change arrives, and the recorded content is a different size.
+        mTask.setBounds(new Rect(0, 0, recordedWidth, recordedHeight));
+        mContentRecorder.onConfigurationChanged(mDefaultDisplay.getLastOrientation());
+        assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
+
+        // THEN content in the captured DisplayArea is scaled to fit the surface size.
+        verify(mTransaction, atLeastOnce()).setMatrix(eq(mRecordedSurface), anyFloat(), eq(0f),
+                eq(0f),
+                anyFloat());
+        // THEN the resize callback is notified.
+        verify(mMediaProjectionManagerWrapper).notifyActiveProjectionCapturedContentResized(
+                recordedWidth, recordedHeight);
+    }
+
+    @Test
     public void testPauseRecording_pausesRecording() {
         mContentRecorder.setContentRecordingSession(mDisplaySession);
         mContentRecorder.updateRecording();
@@ -324,6 +349,9 @@
         int scaledWidth = Math.round((float) displayAreaBounds.width() / xScale);
         int xInset = (sSurfaceSize.x - scaledWidth) / 2;
         verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, xInset, 0);
+        // THEN the resize callback is notified.
+        verify(mMediaProjectionManagerWrapper).notifyActiveProjectionCapturedContentResized(
+                displayAreaBounds.width(), displayAreaBounds.height());
     }
 
     private static class RecordingTestToken extends Binder {
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 ba68a25..2ce1d60 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -193,6 +193,30 @@
         assertEquals(result, START_ABORTED);
     }
 
+    @Test
+    public void testCanActivityBeLaunched_requiredDisplayCategory() {
+        ActivityStarter starter = new ActivityStarter(mock(ActivityStartController.class), mAtm,
+                mSupervisor, mock(ActivityStartInterceptor.class));
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(mSecondaryDisplay).build();
+        final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).setTask(task).build();
+        final ActivityRecord disallowedRecord =
+                new ActivityBuilder(mAtm).setRequiredDisplayCategory("auto").build();
+
+        int result = starter.startActivityInner(
+                disallowedRecord,
+                sourceRecord,
+                /* voiceSession= */null,
+                /* voiceInteractor= */ null,
+                /* startFlags= */ 0,
+                /* options= */null,
+                /* inTask= */null,
+                /* inTaskFragment= */ null,
+                /* restrictedBgActivity= */false,
+                /* intentGrants= */null);
+
+        assertEquals(result, START_ABORTED);
+    }
+
     private class TestDisplayWindowPolicyController extends DisplayWindowPolicyController {
 
         public ComponentName DISALLOWED_ACTIVITY =
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 13ea99a..6877e4f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -166,6 +166,114 @@
     }
 
     @Test
+    public void testApplyStrategyToTranslucentActivities() {
+        mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
+        setUpDisplaySizeWithApp(2000, 1000);
+        prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
+        mActivity.info.setMinAspectRatio(1.2f);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        // Translucent Activity
+        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setLaunchedFromUid(mActivity.getUid())
+                .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
+                .setMinAspectRatio(1.1f)
+                .setMaxAspectRatio(3f)
+                .build();
+        doReturn(false).when(translucentActivity).fillsParent();
+        mTask.addChild(translucentActivity);
+        // We check bounds
+        final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
+        final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds();
+        assertEquals(opaqueBounds, translucentRequestedBounds);
+        // We check orientation
+        final int translucentOrientation =
+                translucentActivity.getRequestedConfigurationOrientation();
+        assertEquals(ORIENTATION_PORTRAIT, translucentOrientation);
+        // We check aspect ratios
+        assertEquals(1.2f, translucentActivity.getMinAspectRatio(), 0.00001f);
+        assertEquals(1.5f, translucentActivity.getMaxAspectRatio(), 0.00001f);
+    }
+
+    @Test
+    public void testNotApplyStrategyToTranslucentActivitiesWithDifferentUid() {
+        mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
+        setUpDisplaySizeWithApp(2000, 1000);
+        prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
+        mActivity.info.setMinAspectRatio(1.2f);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        // Translucent Activity
+        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
+                .setMinAspectRatio(1.1f)
+                .setMaxAspectRatio(3f)
+                .build();
+        doReturn(false).when(translucentActivity).fillsParent();
+        mTask.addChild(translucentActivity);
+        // We check bounds
+        final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
+        final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds();
+        assertNotEquals(opaqueBounds, translucentRequestedBounds);
+    }
+
+    @Test
+    public void testApplyStrategyToMultipleTranslucentActivities() {
+        mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
+        setUpDisplaySizeWithApp(2000, 1000);
+        prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
+        mActivity.info.setMinAspectRatio(1.2f);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        // Translucent Activity
+        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setLaunchedFromUid(mActivity.getUid())
+                .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
+                .setMinAspectRatio(1.1f)
+                .setMaxAspectRatio(3f)
+                .build();
+        doReturn(false).when(translucentActivity).fillsParent();
+        mTask.addChild(translucentActivity);
+        // We check bounds
+        final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
+        final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds();
+        assertEquals(opaqueBounds, translucentRequestedBounds);
+        // Launch another translucent activity
+        final ActivityRecord translucentActivity2 = new ActivityBuilder(mAtm)
+                .setLaunchedFromUid(mActivity.getUid())
+                .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
+                .build();
+        doReturn(false).when(translucentActivity2).fillsParent();
+        mTask.addChild(translucentActivity2);
+        // We check bounds
+        final Rect translucent2RequestedBounds = translucentActivity2.getRequestedOverrideBounds();
+        assertEquals(opaqueBounds, translucent2RequestedBounds);
+    }
+
+    @Test
+    public void testTranslucentActivitiesDontGoInSizeCompactMode() {
+        mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
+        setUpDisplaySizeWithApp(2800, 1400);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        prepareUnresizable(mActivity, -1f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
+        // Rotate to put activity in size compat mode.
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+        assertTrue(mActivity.inSizeCompatMode());
+        // Rotate back
+        rotateDisplay(mActivity.mDisplayContent, ROTATION_0);
+        assertFalse(mActivity.inSizeCompatMode());
+        // We launch a transparent activity
+        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setLaunchedFromUid(mActivity.getUid())
+                .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+                .build();
+        doReturn(true).when(translucentActivity).fillsParent();
+        mTask.addChild(translucentActivity);
+        // It should not be in SCM
+        assertFalse(translucentActivity.inSizeCompatMode());
+        // We rotate again
+        rotateDisplay(translucentActivity.mDisplayContent, ROTATION_90);
+        assertFalse(translucentActivity.inSizeCompatMode());
+    }
+
+    @Test
     public void testRestartProcessIfVisible() {
         setUpDisplaySizeWithApp(1000, 2500);
         doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
index 5e1fae0..7d9f29c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
@@ -20,6 +20,9 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
 import android.platform.test.annotations.Presubmit;
 import android.view.SurfaceControl;
@@ -31,12 +34,15 @@
 import org.junit.Test;
 
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
 @SmallTest
 @Presubmit
 public class SurfaceSyncGroupTest {
 
+    private final Executor mExecutor = Runnable::run;
+
     @Before
     public void setup() {
         SurfaceSyncGroup.setTransactionFactory(StubTransaction::new);
@@ -45,10 +51,11 @@
     @Test
     public void testSyncOne() throws InterruptedException {
         final CountDownLatch finishedLatch = new CountDownLatch(1);
-        SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(transaction -> finishedLatch.countDown());
+        SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+        syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
         SyncTarget syncTarget = new SyncTarget();
-        syncGroup.addToSync(syncTarget);
-        syncGroup.markSyncReady();
+        syncGroup.addToSync(syncTarget, false /* parentSyncGroupMerge */);
+        syncGroup.onTransactionReady(null);
 
         syncTarget.onBufferReady();
 
@@ -59,15 +66,16 @@
     @Test
     public void testSyncMultiple() throws InterruptedException {
         final CountDownLatch finishedLatch = new CountDownLatch(1);
-        SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(transaction -> finishedLatch.countDown());
+        SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+        syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
         SyncTarget syncTarget1 = new SyncTarget();
         SyncTarget syncTarget2 = new SyncTarget();
         SyncTarget syncTarget3 = new SyncTarget();
 
-        syncGroup.addToSync(syncTarget1);
-        syncGroup.addToSync(syncTarget2);
-        syncGroup.addToSync(syncTarget3);
-        syncGroup.markSyncReady();
+        syncGroup.addToSync(syncTarget1, false /* parentSyncGroupMerge */);
+        syncGroup.addToSync(syncTarget2, false /* parentSyncGroupMerge */);
+        syncGroup.addToSync(syncTarget3, false /* parentSyncGroupMerge */);
+        syncGroup.onTransactionReady(null);
 
         syncTarget1.onBufferReady();
         assertNotEquals(0, finishedLatch.getCount());
@@ -83,35 +91,35 @@
 
     @Test
     public void testAddSyncWhenSyncComplete() {
-        final CountDownLatch finishedLatch = new CountDownLatch(1);
-        SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(transaction -> finishedLatch.countDown());
+        SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
 
         SyncTarget syncTarget1 = new SyncTarget();
         SyncTarget syncTarget2 = new SyncTarget();
 
-        assertTrue(syncGroup.addToSync(syncTarget1));
-        syncGroup.markSyncReady();
+        assertTrue(syncGroup.addToSync(syncTarget1, false /* parentSyncGroupMerge */));
+        syncGroup.onTransactionReady(null);
         // Adding to a sync that has been completed is also invalid since the sync id has been
         // cleared.
-        assertFalse(syncGroup.addToSync(syncTarget2));
+        assertFalse(syncGroup.addToSync(syncTarget2, false /* parentSyncGroupMerge */));
     }
 
     @Test
-    public void testMultiplesyncGroups() throws InterruptedException {
+    public void testMultipleSyncGroups() throws InterruptedException {
         final CountDownLatch finishedLatch1 = new CountDownLatch(1);
         final CountDownLatch finishedLatch2 = new CountDownLatch(1);
-        SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(
-                transaction -> finishedLatch1.countDown());
-        SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(
-                transaction -> finishedLatch2.countDown());
+        SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup();
+        SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup();
+
+        syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
+        syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
 
         SyncTarget syncTarget1 = new SyncTarget();
         SyncTarget syncTarget2 = new SyncTarget();
 
-        assertTrue(syncGroup1.addToSync(syncTarget1));
-        assertTrue(syncGroup2.addToSync(syncTarget2));
-        syncGroup1.markSyncReady();
-        syncGroup2.markSyncReady();
+        assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */));
+        assertTrue(syncGroup2.addToSync(syncTarget2, false /* parentSyncGroupMerge */));
+        syncGroup1.onTransactionReady(null);
+        syncGroup2.onTransactionReady(null);
 
         syncTarget1.onBufferReady();
 
@@ -126,22 +134,23 @@
     }
 
     @Test
-    public void testMergeSync() throws InterruptedException {
+    public void testAddSyncGroup() throws InterruptedException {
         final CountDownLatch finishedLatch1 = new CountDownLatch(1);
         final CountDownLatch finishedLatch2 = new CountDownLatch(1);
-        SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(
-                transaction -> finishedLatch1.countDown());
-        SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(
-                transaction -> finishedLatch2.countDown());
+        SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup();
+        SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup();
+
+        syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
+        syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
 
         SyncTarget syncTarget1 = new SyncTarget();
         SyncTarget syncTarget2 = new SyncTarget();
 
-        assertTrue(syncGroup1.addToSync(syncTarget1));
-        assertTrue(syncGroup2.addToSync(syncTarget2));
-        syncGroup1.markSyncReady();
-        syncGroup2.merge(syncGroup1);
-        syncGroup2.markSyncReady();
+        assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */));
+        assertTrue(syncGroup2.addToSync(syncTarget2, false /* parentSyncGroupMerge */));
+        syncGroup1.onTransactionReady(null);
+        syncGroup2.addToSync(syncGroup1, false /* parentSyncGroupMerge */);
+        syncGroup2.onTransactionReady(null);
 
         // Finish syncTarget2 first to test that the syncGroup is not complete until the merged sync
         // is also done.
@@ -161,28 +170,29 @@
     }
 
     @Test
-    public void testMergeSyncAlreadyComplete() throws InterruptedException {
+    public void testAddSyncAlreadyComplete() throws InterruptedException {
         final CountDownLatch finishedLatch1 = new CountDownLatch(1);
         final CountDownLatch finishedLatch2 = new CountDownLatch(1);
-        SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(
-                transaction -> finishedLatch1.countDown());
-        SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(
-                transaction -> finishedLatch2.countDown());
+        SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup();
+        SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup();
+
+        syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
+        syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
 
         SyncTarget syncTarget1 = new SyncTarget();
         SyncTarget syncTarget2 = new SyncTarget();
 
-        assertTrue(syncGroup1.addToSync(syncTarget1));
-        assertTrue(syncGroup2.addToSync(syncTarget2));
-        syncGroup1.markSyncReady();
+        assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */));
+        assertTrue(syncGroup2.addToSync(syncTarget2, false /* parentSyncGroupMerge */));
+        syncGroup1.onTransactionReady(null);
         syncTarget1.onBufferReady();
 
         // The first sync will still get a callback when it's sync requirements are done.
         finishedLatch1.await(5, TimeUnit.SECONDS);
         assertEquals(0, finishedLatch1.getCount());
 
-        syncGroup2.merge(syncGroup1);
-        syncGroup2.markSyncReady();
+        syncGroup2.addToSync(syncGroup1, false /* parentSyncGroupMerge */);
+        syncGroup2.onTransactionReady(null);
         syncTarget2.onBufferReady();
 
         // Verify that the second sync will receive complete since the merged sync was already
@@ -191,18 +201,145 @@
         assertEquals(0, finishedLatch2.getCount());
     }
 
-    private static class SyncTarget implements SurfaceSyncGroup.SyncTarget {
-        private SurfaceSyncGroup.TransactionReadyCallback mTransactionReadyCallback;
+    @Test
+    public void testAddSyncAlreadyInASync_NewSyncReadyFirst() throws InterruptedException {
+        final CountDownLatch finishedLatch1 = new CountDownLatch(1);
+        final CountDownLatch finishedLatch2 = new CountDownLatch(1);
+        SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup();
+        SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup();
 
-        @Override
-        public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
-                SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) {
-            mTransactionReadyCallback = transactionReadyCallback;
-        }
+        syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
+        syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
 
+        SyncTarget syncTarget1 = new SyncTarget();
+        SyncTarget syncTarget2 = new SyncTarget();
+        SyncTarget syncTarget3 = new SyncTarget();
+
+        assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */));
+        assertTrue(syncGroup1.addToSync(syncTarget2, false /* parentSyncGroupMerge */));
+
+        // Add syncTarget1 to syncGroup2 so it forces syncGroup1 into syncGroup2
+        assertTrue(syncGroup2.addToSync(syncTarget1, false /* parentSyncGroupMerge */));
+        assertTrue(syncGroup2.addToSync(syncTarget3, false /* parentSyncGroupMerge */));
+
+        syncGroup1.onTransactionReady(null);
+        syncGroup2.onTransactionReady(null);
+
+        // Make target1 and target3 ready, but not target2. SyncGroup2 should not be ready since
+        // SyncGroup2 also waits for all of SyncGroup1 to finish, which includes target2
+        syncTarget1.onBufferReady();
+        syncTarget3.onBufferReady();
+
+        // Neither SyncGroup will be ready.
+        finishedLatch1.await(1, TimeUnit.SECONDS);
+        finishedLatch2.await(1, TimeUnit.SECONDS);
+
+        assertEquals(1, finishedLatch1.getCount());
+        assertEquals(1, finishedLatch2.getCount());
+
+        syncTarget2.onBufferReady();
+
+        // Both sync groups should be ready after target2 completed.
+        finishedLatch1.await(5, TimeUnit.SECONDS);
+        finishedLatch2.await(5, TimeUnit.SECONDS);
+        assertEquals(0, finishedLatch1.getCount());
+        assertEquals(0, finishedLatch2.getCount());
+    }
+
+    @Test
+    public void testAddSyncAlreadyInASync_OldSyncFinishesFirst() throws InterruptedException {
+        final CountDownLatch finishedLatch1 = new CountDownLatch(1);
+        final CountDownLatch finishedLatch2 = new CountDownLatch(1);
+        SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup();
+        SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup();
+
+        syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
+        syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
+
+        SyncTarget syncTarget1 = new SyncTarget();
+        SyncTarget syncTarget2 = new SyncTarget();
+        SyncTarget syncTarget3 = new SyncTarget();
+
+        assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */));
+        assertTrue(syncGroup1.addToSync(syncTarget2, false /* parentSyncGroupMerge */));
+        syncTarget2.onBufferReady();
+
+        // Add syncTarget1 to syncGroup2 so it forces syncGroup1 into syncGroup2
+        assertTrue(syncGroup2.addToSync(syncTarget1, false /* parentSyncGroupMerge */));
+        assertTrue(syncGroup2.addToSync(syncTarget3, false /* parentSyncGroupMerge */));
+
+        syncGroup1.onTransactionReady(null);
+        syncGroup2.onTransactionReady(null);
+
+        syncTarget1.onBufferReady();
+
+        // Only SyncGroup1 will be ready, but SyncGroup2 still needs its own targets to be ready.
+        finishedLatch1.await(1, TimeUnit.SECONDS);
+        finishedLatch2.await(1, TimeUnit.SECONDS);
+
+        assertEquals(0, finishedLatch1.getCount());
+        assertEquals(1, finishedLatch2.getCount());
+
+        syncTarget3.onBufferReady();
+
+        // SyncGroup2 is finished after target3 completed.
+        finishedLatch2.await(1, TimeUnit.SECONDS);
+        assertEquals(0, finishedLatch2.getCount());
+    }
+
+    @Test
+    public void testParentSyncGroupMerge_true() {
+        // Temporarily set a new transaction factory so it will return the stub transaction for
+        // the sync group.
+        SurfaceControl.Transaction parentTransaction = spy(new StubTransaction());
+        SurfaceSyncGroup.setTransactionFactory(() -> parentTransaction);
+
+        final CountDownLatch finishedLatch = new CountDownLatch(1);
+        SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+        syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
+
+        SurfaceControl.Transaction targetTransaction = spy(new StubTransaction());
+        SurfaceSyncGroup.setTransactionFactory(() -> targetTransaction);
+
+        SyncTarget syncTarget = new SyncTarget();
+        assertTrue(syncGroup.addToSync(syncTarget, true /* parentSyncGroupMerge */));
+        syncTarget.onTransactionReady(null);
+
+        // When parentSyncGroupMerge is true, the transaction passed in merges the main SyncGroup
+        // transaction first because it knows the previous parentSyncGroup is older so it should
+        // be overwritten by anything newer.
+        verify(targetTransaction).merge(parentTransaction);
+        verify(parentTransaction).merge(targetTransaction);
+    }
+
+    @Test
+    public void testParentSyncGroupMerge_false() {
+        // Temporarily set a new transaction factory so it will return the stub transaction for
+        // the sync group.
+        SurfaceControl.Transaction parentTransaction = spy(new StubTransaction());
+        SurfaceSyncGroup.setTransactionFactory(() -> parentTransaction);
+
+        final CountDownLatch finishedLatch = new CountDownLatch(1);
+        SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+        syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
+
+        SurfaceControl.Transaction targetTransaction = spy(new StubTransaction());
+        SurfaceSyncGroup.setTransactionFactory(() -> targetTransaction);
+
+        SyncTarget syncTarget = new SyncTarget();
+        assertTrue(syncGroup.addToSync(syncTarget, false /* parentSyncGroupMerge */));
+        syncTarget.onTransactionReady(null);
+
+        // When parentSyncGroupMerge is false, the transaction passed in should not merge
+        // the main SyncGroup since we don't need to change the transaction order
+        verify(targetTransaction, never()).merge(parentTransaction);
+        verify(parentTransaction).merge(targetTransaction);
+    }
+
+    private static class SyncTarget extends SurfaceSyncGroup {
         void onBufferReady() {
             SurfaceControl.Transaction t = new StubTransaction();
-            mTransactionReadyCallback.onTransactionReady(t);
+            onTransactionReady(t);
         }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java
index 99ceb20..3e74626 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java
@@ -21,6 +21,7 @@
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 
+import static com.android.server.wm.utils.CoordinateTransforms.computeRotationMatrix;
 import static com.android.server.wm.utils.CoordinateTransforms.transformLogicalToPhysicalCoordinates;
 import static com.android.server.wm.utils.CoordinateTransforms.transformPhysicalToLogicalCoordinates;
 import static com.android.server.wm.utils.CoordinateTransforms.transformToRotation;
@@ -185,6 +186,44 @@
         assertEquals(mMatrix2, mMatrix);
     }
 
+    @Test
+    public void rotate_0_bottomRight() {
+        computeRotationMatrix(ROTATION_0, W, H, mMatrix);
+        PointF newPoints = checkMappedPoints(W, H);
+        assertEquals(W, newPoints.x, 0);
+        assertEquals(H, newPoints.y, 0);
+    }
+
+    @Test
+    public void rotate_90_bottomRight() {
+        computeRotationMatrix(ROTATION_90, W, H, mMatrix);
+        PointF newPoints = checkMappedPoints(W, H);
+        assertEquals(0, newPoints.x, 0);
+        assertEquals(W, newPoints.y, 0);
+    }
+
+    @Test
+    public void rotate_180_bottomRight() {
+        computeRotationMatrix(ROTATION_180, W, H, mMatrix);
+        PointF newPoints = checkMappedPoints(W, H);
+        assertEquals(0, newPoints.x, 0);
+        assertEquals(0, newPoints.y, 0);
+    }
+
+    @Test
+    public void rotate_270_bottomRight() {
+        computeRotationMatrix(ROTATION_270, W, H, mMatrix);
+        PointF newPoints = checkMappedPoints(W, H);
+        assertEquals(H, newPoints.x, 0);
+        assertEquals(0, newPoints.y, 0);
+    }
+
+    private PointF checkMappedPoints(int x, int y) {
+        final float[] fs = new float[] {x, y};
+        mMatrix.mapPoints(fs);
+        return new PointF(fs[0], fs[1]);
+    }
+
     private void assertMatricesAreInverses(Matrix matrix, Matrix matrix2) {
         final Matrix concat = new Matrix();
         concat.setConcat(matrix, matrix2);
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
index cd4d65d..ff43ff7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
@@ -23,15 +23,11 @@
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.ColorSpace;
-import android.graphics.Matrix;
-import android.graphics.PointF;
 import android.hardware.HardwareBuffer;
 import android.platform.test.annotations.Presubmit;
-import android.view.Surface;
 
 import com.android.internal.policy.TransitionAnimation;
 
-import org.junit.Before;
 import org.junit.Test;
 
 @Presubmit
@@ -39,16 +35,8 @@
 
     private static final int BITMAP_HEIGHT = 100;
     private static final int BITMAP_WIDTH = 100;
-    private static final int POINT_WIDTH = 1000;
-    private static final int POINT_HEIGHT = 2000;
 
     private ColorSpace mColorSpace = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
-    private Matrix mMatrix;
-
-    @Before
-    public void setup() {
-        mMatrix = new Matrix();
-    }
 
     @Test
     public void blackLuma() {
@@ -93,48 +81,6 @@
         assertEquals(1, borderLuma, 0);
     }
 
-    @Test
-    public void rotate_0_bottomRight() {
-        RotationAnimationUtils.createRotationMatrix(Surface.ROTATION_0,
-                POINT_WIDTH, POINT_HEIGHT, mMatrix);
-        PointF newPoints = checkMappedPoints(POINT_WIDTH, POINT_HEIGHT);
-        assertEquals(POINT_WIDTH, newPoints.x, 0);
-        assertEquals(POINT_HEIGHT, newPoints.y, 0);
-    }
-
-    @Test
-    public void rotate_90_bottomRight() {
-        RotationAnimationUtils.createRotationMatrix(Surface.ROTATION_90,
-                POINT_WIDTH, POINT_HEIGHT, mMatrix);
-        PointF newPoints = checkMappedPoints(POINT_WIDTH, POINT_HEIGHT);
-        assertEquals(0, newPoints.x, 0);
-        assertEquals(POINT_WIDTH, newPoints.y, 0);
-    }
-
-    @Test
-    public void rotate_180_bottomRight() {
-        RotationAnimationUtils.createRotationMatrix(Surface.ROTATION_180,
-                POINT_WIDTH, POINT_HEIGHT, mMatrix);
-        PointF newPoints = checkMappedPoints(POINT_WIDTH, POINT_HEIGHT);
-        assertEquals(0, newPoints.x, 0);
-        assertEquals(0, newPoints.y, 0);
-    }
-
-    @Test
-    public void rotate_270_bottomRight() {
-        RotationAnimationUtils.createRotationMatrix(Surface.ROTATION_270,
-                POINT_WIDTH, POINT_HEIGHT, mMatrix);
-        PointF newPoints = checkMappedPoints(POINT_WIDTH, POINT_HEIGHT);
-        assertEquals(POINT_HEIGHT, newPoints.x, 0);
-        assertEquals(0, newPoints.y, 0);
-    }
-
-    private PointF checkMappedPoints(int x, int y) {
-        final float[] fs = new float[] {x, y};
-        mMatrix.mapPoints(fs);
-        return new PointF(fs[0], fs[1]);
-    }
-
     private Bitmap createBitmap(float luma) {
         return createBitmap(luma, BITMAP_WIDTH, BITMAP_HEIGHT);
     }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
index 2812264..84bd7160 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.hardware.soundtrigger.SoundTrigger;
 import android.media.permission.Identity;
+import android.os.IBinder;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.SharedMemory;
@@ -74,12 +75,13 @@
 
     DspTrustedHotwordDetectorSession(
             @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService,
-            @NonNull Object lock, @NonNull Context context,
+            @NonNull Object lock, @NonNull Context context, @NonNull IBinder token,
             @NonNull IHotwordRecognitionStatusCallback callback, int voiceInteractionServiceUid,
             Identity voiceInteractorIdentity,
             @NonNull ScheduledExecutorService scheduledExecutorService, boolean logging) {
-        super(remoteHotwordDetectionService, lock, context, callback, voiceInteractionServiceUid,
-                voiceInteractorIdentity, scheduledExecutorService, logging);
+        super(remoteHotwordDetectionService, lock, context, token, callback,
+                voiceInteractionServiceUid, voiceInteractorIdentity, scheduledExecutorService,
+                logging);
     }
 
     @SuppressWarnings("GuardedBy")
@@ -106,12 +108,14 @@
                     }
                     HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                             HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
-                            HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED);
+                            HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED,
+                            mVoiceInteractionServiceUid);
                     if (!mValidatingDspTrigger) {
                         Slog.i(TAG, "Ignoring #onDetected due to a process restart");
                         HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
-                                METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK);
+                                METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK,
+                                mVoiceInteractionServiceUid);
                         return;
                     }
                     mValidatingDspTrigger = false;
@@ -122,7 +126,8 @@
                         Slog.i(TAG, "Ignoring #onDetected due to a SecurityException", e);
                         HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
-                                METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION);
+                                METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION,
+                                mVoiceInteractionServiceUid);
                         externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
                         return;
                     }
@@ -157,12 +162,14 @@
                     }
                     HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                             HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
-                            HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED);
+                            HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED,
+                            mVoiceInteractionServiceUid);
                     if (!mValidatingDspTrigger) {
                         Slog.i(TAG, "Ignoring #onRejected due to a process restart");
                         HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
-                                METRICS_KEYPHRASE_TRIGGERED_REJECT_UNEXPECTED_CALLBACK);
+                                METRICS_KEYPHRASE_TRIGGERED_REJECT_UNEXPECTED_CALLBACK,
+                                mVoiceInteractionServiceUid);
                         return;
                     }
                     mValidatingDspTrigger = false;
@@ -187,7 +194,8 @@
                         Slog.w(TAG, "Timed out on #detectFromDspSource");
                         HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
-                                HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT);
+                                HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT,
+                                mVoiceInteractionServiceUid);
                         try {
                             externalCallback.onError(CALLBACK_DETECT_TIMEOUT);
                         } catch (RemoteException e) {
@@ -220,7 +228,8 @@
                 mCallback.onRejected(new HotwordRejectedResult.Builder().build());
                 HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                         HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
-                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED_FROM_RESTART);
+                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED_FROM_RESTART,
+                        mVoiceInteractionServiceUid);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to call #rejected");
                 HotwordMetricsLogger.writeDetectorEvent(
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 2a4d4a1..6a7a2f9 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -49,11 +49,12 @@
 import android.provider.DeviceConfig;
 import android.service.voice.HotwordDetectionService;
 import android.service.voice.HotwordDetector;
-import android.service.voice.IHotwordDetectionService;
 import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
+import android.service.voice.ISandboxedDetectionService;
 import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity;
 import android.speech.IRecognitionServiceManager;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.view.contentcapture.IContentCaptureManager;
 
 import com.android.internal.annotations.GuardedBy;
@@ -68,6 +69,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
 /**
@@ -113,18 +115,18 @@
     @GuardedBy("mLock")
     private boolean mDebugHotwordLogging = false;
 
+    /**
+     * For multiple detectors feature, we only support one AlwaysOnHotwordDetector and one
+     * SoftwareHotwordDetector at the same time. We use SparseArray with detector type as the key
+     * to record the detectors.
+     */
     @GuardedBy("mLock")
-    private final HotwordDetectorSession mHotwordDetectorSession;
+    private final SparseArray<HotwordDetectorSession> mHotwordDetectorSessions =
+            new SparseArray<>();
 
     HotwordDetectionConnection(Object lock, Context context, int voiceInteractionServiceUid,
             Identity voiceInteractorIdentity, ComponentName serviceName, int userId,
-            boolean bindInstantServiceAllowed, @Nullable PersistableBundle options,
-            @Nullable SharedMemory sharedMemory,
-            @NonNull IHotwordRecognitionStatusCallback callback, int detectorType) {
-        if (callback == null) {
-            Slog.w(TAG, "Callback is null while creating connection");
-            throw new IllegalArgumentException("Callback is null while creating connection");
-        }
+            boolean bindInstantServiceAllowed, int detectorType) {
         mLock = lock;
         mContext = context;
         mVoiceInteractionServiceUid = voiceInteractionServiceUid;
@@ -142,19 +144,6 @@
         mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked();
         mLastRestartInstant = Instant.now();
 
-        if (detectorType == HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP) {
-            mHotwordDetectorSession = new DspTrustedHotwordDetectorSession(
-                    mRemoteHotwordDetectionService, mLock, mContext, callback,
-                    mVoiceInteractionServiceUid, mVoiceInteractorIdentity,
-                    mScheduledExecutorService, mDebugHotwordLogging);
-        } else {
-            mHotwordDetectorSession = new SoftwareTrustedHotwordDetectorSession(
-                    mRemoteHotwordDetectionService, mLock, mContext, callback,
-                    mVoiceInteractionServiceUid, mVoiceInteractorIdentity,
-                    mScheduledExecutorService, mDebugHotwordLogging);
-        }
-        mHotwordDetectorSession.initialize(options, sharedMemory);
-
         if (mReStartPeriodSeconds <= 0) {
             mCancellationTaskFuture = null;
         } else {
@@ -165,7 +154,8 @@
                 synchronized (mLock) {
                     restartProcessLocked();
                     HotwordMetricsLogger.writeServiceRestartEvent(mDetectorType,
-                            HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE);
+                            HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE,
+                            mVoiceInteractionServiceUid);
                 }
             }, mReStartPeriodSeconds, mReStartPeriodSeconds, TimeUnit.SECONDS);
         }
@@ -200,7 +190,8 @@
             // conditions with audio reading in the service.
             restartProcessLocked();
             HotwordMetricsLogger.writeServiceRestartEvent(mDetectorType,
-                    HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED);
+                    HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED,
+                    mVoiceInteractionServiceUid);
         }
     }
 
@@ -208,7 +199,10 @@
     void cancelLocked() {
         Slog.v(TAG, "cancelLocked");
         clearDebugHotwordLoggingTimeoutLocked();
-        mHotwordDetectorSession.destroyLocked();
+        runForEachHotwordDetectorSessionLocked((session) -> {
+            session.destroyLocked();
+        });
+        mHotwordDetectorSessions.clear();
         mDebugHotwordLogging = false;
         mRemoteHotwordDetectionService.unbind();
         LocalServices.getService(PermissionManagerServiceInternal.class)
@@ -218,7 +212,7 @@
         }
         mIdentity = null;
         if (mCancellationTaskFuture != null) {
-            mCancellationTaskFuture.cancel(/* may interrupt */ true);
+            mCancellationTaskFuture.cancel(/* mayInterruptIfRunning= */ true);
         }
         if (mAudioFlinger != null) {
             mAudioFlinger.unlinkToDeath(mAudioServerDeathRecipient, /* flags= */ 0);
@@ -226,57 +220,65 @@
     }
 
     @SuppressWarnings("GuardedBy")
-    void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory) {
-        mHotwordDetectorSession.updateStateLocked(options, sharedMemory, mLastRestartInstant);
+    void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory,
+            @NonNull IBinder token) {
+        final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token);
+        if (session == null) {
+            Slog.v(TAG, "Not found the detector by token");
+            return;
+        }
+        session.updateStateLocked(options, sharedMemory, mLastRestartInstant);
     }
 
     /**
      * This method is only used by SoftwareHotwordDetector.
      */
-    void startListeningFromMic(
+    void startListeningFromMicLocked(
             AudioFormat audioFormat,
             IMicrophoneHotwordDetectionVoiceInteractionCallback callback) {
         if (DEBUG) {
-            Slog.d(TAG, "startListeningFromMic");
+            Slog.d(TAG, "startListeningFromMicLocked");
         }
-        synchronized (mLock) {
-            if (!(mHotwordDetectorSession instanceof SoftwareTrustedHotwordDetectorSession)) {
-                Slog.d(TAG, "It is not a software detector");
-                return;
-            }
-            ((SoftwareTrustedHotwordDetectorSession) mHotwordDetectorSession)
-                    .startListeningFromMicLocked(audioFormat, callback);
+        // We only support one Dsp trusted hotword detector and one software hotword detector at
+        // the same time, so we can reuse original single software trusted hotword mechanism.
+        final SoftwareTrustedHotwordDetectorSession session =
+                getSoftwareTrustedHotwordDetectorSessionLocked();
+        if (session == null) {
+            return;
         }
+        session.startListeningFromMicLocked(audioFormat, callback);
     }
 
-    public void startListeningFromExternalSource(
+    public void startListeningFromExternalSourceLocked(
             ParcelFileDescriptor audioStream,
             AudioFormat audioFormat,
             @Nullable PersistableBundle options,
+            @NonNull IBinder token,
             IMicrophoneHotwordDetectionVoiceInteractionCallback callback) {
         if (DEBUG) {
-            Slog.d(TAG, "startListeningFromExternalSource");
+            Slog.d(TAG, "startListeningFromExternalSourceLocked");
         }
-        synchronized (mLock) {
-            mHotwordDetectorSession.startListeningFromExternalSourceLocked(audioStream, audioFormat,
-                    options, callback);
+        final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token);
+        if (session == null) {
+            Slog.v(TAG, "Not found the detector by token");
+            return;
         }
+        session.startListeningFromExternalSourceLocked(audioStream, audioFormat, options, callback);
     }
 
     /**
      * This method is only used by SoftwareHotwordDetector.
      */
-    void stopListening() {
+    void stopListeningFromMicLocked() {
         if (DEBUG) {
-            Slog.d(TAG, "stopListening");
+            Slog.d(TAG, "stopListeningFromMicLocked");
         }
-        synchronized (mLock) {
-            if (!(mHotwordDetectorSession instanceof SoftwareTrustedHotwordDetectorSession)) {
-                Slog.d(TAG, "It is not a software detector");
-                return;
-            }
-            ((SoftwareTrustedHotwordDetectorSession) mHotwordDetectorSession).stopListeningLocked();
+        final SoftwareTrustedHotwordDetectorSession session =
+                getSoftwareTrustedHotwordDetectorSessionLocked();
+        if (session == null) {
+            return;
         }
+        session.stopListeningFromMicLocked();
     }
 
     void triggerHardwareRecognitionEventForTestLocked(
@@ -293,13 +295,16 @@
         if (DEBUG) {
             Slog.d(TAG, "detectFromDspSource");
         }
+        // We only support one Dsp trusted hotword detector and one software hotword detector at
+        // the same time, so we can reuse original single Dsp trusted hotword mechanism.
         synchronized (mLock) {
-            if (!(mHotwordDetectorSession instanceof DspTrustedHotwordDetectorSession)) {
-                Slog.d(TAG, "It is not a Dsp detector");
+            final DspTrustedHotwordDetectorSession session =
+                    getDspTrustedHotwordDetectorSessionLocked();
+            if (session == null || !session.isSameCallback(externalCallback)) {
+                Slog.v(TAG, "Not found the Dsp detector by callback");
                 return;
             }
-            ((DspTrustedHotwordDetectorSession) mHotwordDetectorSession).detectFromDspSourceLocked(
-                    recognitionEvent, externalCallback);
+            session.detectFromDspSourceLocked(recognitionEvent, externalCallback);
         }
     }
 
@@ -315,7 +320,9 @@
         Slog.v(TAG, "setDebugHotwordLoggingLocked: " + logging);
         clearDebugHotwordLoggingTimeoutLocked();
         mDebugHotwordLogging = logging;
-        mHotwordDetectorSession.setDebugHotwordLoggingLocked(logging);
+        runForEachHotwordDetectorSessionLocked((session) -> {
+            session.setDebugHotwordLoggingLocked(logging);
+        });
 
         if (logging) {
             // Reset mDebugHotwordLogging to false after one hour
@@ -323,7 +330,9 @@
                 Slog.v(TAG, "Timeout to reset mDebugHotwordLogging to false");
                 synchronized (mLock) {
                     mDebugHotwordLogging = false;
-                    mHotwordDetectorSession.setDebugHotwordLoggingLocked(false);
+                    runForEachHotwordDetectorSessionLocked((session) -> {
+                        session.setDebugHotwordLoggingLocked(false);
+                    });
                 }
             }, RESET_DEBUG_HOTWORD_LOGGING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
         }
@@ -331,7 +340,7 @@
 
     private void clearDebugHotwordLoggingTimeoutLocked() {
         if (mDebugHotwordLoggingTimeoutFuture != null) {
-            mDebugHotwordLoggingTimeoutFuture.cancel(/* mayInterruptIfRunning= */true);
+            mDebugHotwordLoggingTimeoutFuture.cancel(/* mayInterruptIfRunning= */ true);
             mDebugHotwordLoggingTimeoutFuture = null;
         }
     }
@@ -348,12 +357,14 @@
         mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked();
 
         Slog.v(TAG, "Started the new process, dispatching processRestarted to detector");
-        mHotwordDetectorSession.updateRemoteHotwordDetectionServiceLocked(
-                mRemoteHotwordDetectionService);
-        mHotwordDetectorSession.informRestartProcessLocked();
+        runForEachHotwordDetectorSessionLocked((session) -> {
+            session.updateRemoteHotwordDetectionServiceLocked(mRemoteHotwordDetectionService);
+            session.informRestartProcessLocked();
+        });
         if (DEBUG) {
             Slog.i(TAG, "processRestarted is dispatched done, unbinding from the old process");
         }
+
         oldConnection.ignoreConnectionStatusEvents();
         oldConnection.unbind();
         if (previousIdentity != null) {
@@ -364,11 +375,13 @@
     static final class SoundTriggerCallback extends IRecognitionStatusCallback.Stub {
         private final HotwordDetectionConnection mHotwordDetectionConnection;
         private final IHotwordRecognitionStatusCallback mExternalCallback;
+        private final int mVoiceInteractionServiceUid;
 
         SoundTriggerCallback(IHotwordRecognitionStatusCallback callback,
-                HotwordDetectionConnection connection) {
+                HotwordDetectionConnection connection, int uid) {
             mHotwordDetectionConnection = connection;
             mExternalCallback = callback;
+            mVoiceInteractionServiceUid = uid;
         }
 
         @Override
@@ -381,13 +394,15 @@
             if (useHotwordDetectionService) {
                 HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                         HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP,
-                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER);
+                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
+                        mVoiceInteractionServiceUid);
                 mHotwordDetectionConnection.detectFromDspSource(
                         recognitionEvent, mExternalCallback);
             } else {
                 HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                         HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
-                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER);
+                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
+                        mVoiceInteractionServiceUid);
                 mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
             }
         }
@@ -425,8 +440,10 @@
             pw.print(prefix); pw.print("mLastRestartInstant="); pw.println(mLastRestartInstant);
             pw.print(prefix); pw.print("mDetectorType=");
             pw.println(HotwordDetector.detectorTypeToString(mDetectorType));
-            pw.print(prefix); pw.println("HotwordDetectorSession");
-            mHotwordDetectorSession.dumpLocked(prefix, pw);
+            pw.print(prefix); pw.println("HotwordDetectorSession(s)");
+            runForEachHotwordDetectorSessionLocked((session) -> {
+                session.dumpLocked(prefix, pw);
+            });
         }
     }
 
@@ -444,7 +461,7 @@
         ServiceConnection createLocked() {
             ServiceConnection connection =
                     new ServiceConnection(mContext, mIntent, mBindingFlags, mUser,
-                            IHotwordDetectionService.Stub::asInterface,
+                            ISandboxedDetectionService.Stub::asInterface,
                             mRestartCount++ % MAX_ISOLATED_PROCESS_NUMBER);
             connection.connect();
 
@@ -456,7 +473,7 @@
         }
     }
 
-    class ServiceConnection extends ServiceConnector.Impl<IHotwordDetectionService> {
+    class ServiceConnection extends ServiceConnector.Impl<ISandboxedDetectionService> {
         private final Object mLock = new Object();
 
         private final Intent mIntent;
@@ -469,7 +486,7 @@
 
         ServiceConnection(@NonNull Context context,
                 @NonNull Intent intent, int bindingFlags, int userId,
-                @Nullable Function<IBinder, IHotwordDetectionService> binderAsInterface,
+                @Nullable Function<IBinder, ISandboxedDetectionService> binderAsInterface,
                 int instanceNumber) {
             super(context, intent, bindingFlags, userId, binderAsInterface);
             this.mIntent = intent;
@@ -478,7 +495,7 @@
         }
 
         @Override // from ServiceConnector.Impl
-        protected void onServiceConnectionStatusChanged(IHotwordDetectionService service,
+        protected void onServiceConnectionStatusChanged(ISandboxedDetectionService service,
                 boolean connected) {
             if (DEBUG) {
                 Slog.d(TAG, "onServiceConnectionStatusChanged connected = " + connected);
@@ -519,8 +536,10 @@
                 }
             }
             synchronized (HotwordDetectionConnection.this.mLock) {
-                mHotwordDetectorSession.reportErrorLocked(
-                        HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED);
+                runForEachHotwordDetectorSessionLocked((session) -> {
+                    session.reportErrorLocked(
+                            HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED);
+                });
             }
         }
 
@@ -565,6 +584,93 @@
         }
     }
 
+    @SuppressWarnings("GuardedBy")
+    void createDetectorLocked(
+            @Nullable PersistableBundle options,
+            @Nullable SharedMemory sharedMemory,
+            @NonNull IBinder token,
+            @NonNull IHotwordRecognitionStatusCallback callback,
+            int detectorType) {
+        // We only support one Dsp trusted hotword detector and one software hotword detector at
+        // the same time, remove existing one.
+        HotwordDetectorSession removeSession = mHotwordDetectorSessions.get(detectorType);
+        if (removeSession != null) {
+            removeSession.destroyLocked();
+            mHotwordDetectorSessions.remove(detectorType);
+        }
+        final HotwordDetectorSession session;
+        if (detectorType == HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP) {
+            session = new DspTrustedHotwordDetectorSession(mRemoteHotwordDetectionService,
+                    mLock, mContext, token, callback, mVoiceInteractionServiceUid,
+                    mVoiceInteractorIdentity, mScheduledExecutorService, mDebugHotwordLogging);
+        } else {
+            session = new SoftwareTrustedHotwordDetectorSession(
+                    mRemoteHotwordDetectionService, mLock, mContext, token, callback,
+                    mVoiceInteractionServiceUid, mVoiceInteractorIdentity,
+                    mScheduledExecutorService, mDebugHotwordLogging);
+        }
+        mHotwordDetectorSessions.put(detectorType, session);
+        session.initialize(options, sharedMemory);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    void destroyDetectorLocked(@NonNull IBinder token) {
+        final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token);
+        if (session != null) {
+            session.destroyLocked();
+            final int index = mHotwordDetectorSessions.indexOfValue(session);
+            if (index < 0 || index > mHotwordDetectorSessions.size() - 1) {
+                return;
+            }
+            mHotwordDetectorSessions.removeAt(index);
+        }
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private HotwordDetectorSession getDetectorSessionByTokenLocked(IBinder token) {
+        if (token == null) {
+            return null;
+        }
+        for (int i = 0; i < mHotwordDetectorSessions.size(); i++) {
+            final HotwordDetectorSession session = mHotwordDetectorSessions.valueAt(i);
+            if (!session.isDestroyed() && session.isSameToken(token)) {
+                return session;
+            }
+        }
+        return null;
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private DspTrustedHotwordDetectorSession getDspTrustedHotwordDetectorSessionLocked() {
+        final HotwordDetectorSession session = mHotwordDetectorSessions.get(
+                HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP);
+        if (session == null || session.isDestroyed()) {
+            Slog.v(TAG, "Not found the Dsp detector");
+            return null;
+        }
+        return (DspTrustedHotwordDetectorSession) session;
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private SoftwareTrustedHotwordDetectorSession getSoftwareTrustedHotwordDetectorSessionLocked() {
+        final HotwordDetectorSession session = mHotwordDetectorSessions.get(
+                HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE);
+        if (session == null || session.isDestroyed()) {
+            Slog.v(TAG, "Not found the software detector");
+            return null;
+        }
+        return (SoftwareTrustedHotwordDetectorSession) session;
+    }
+
+    @SuppressWarnings("GuardedBy")
+    private void runForEachHotwordDetectorSessionLocked(
+            @NonNull Consumer<HotwordDetectorSession> action) {
+        for (int i = 0; i < mHotwordDetectorSessions.size(); i++) {
+            HotwordDetectorSession session = mHotwordDetectorSessions.valueAt(i);
+            action.accept(session);
+        }
+    }
+
     private static void updateAudioFlinger(ServiceConnection connection, IBinder audioFlinger) {
         // TODO: Consider using a proxy that limits the exposed API surface.
         connection.run(service -> service.updateAudioFlinger(audioFlinger));
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java
index f9f43c9..689423a 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java
@@ -57,6 +57,7 @@
 import android.media.permission.PermissionUtil;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.IRemoteCallback;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
@@ -182,16 +183,18 @@
     private boolean mDestroyed = false;
     @GuardedBy("mLock")
     boolean mPerformingExternalSourceHotwordDetection;
+    @NonNull final IBinder mToken;
 
     HotwordDetectorSession(
             @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService,
-            @NonNull Object lock, @NonNull Context context,
+            @NonNull Object lock, @NonNull Context context, @NonNull IBinder token,
             @NonNull IHotwordRecognitionStatusCallback callback, int voiceInteractionServiceUid,
             Identity voiceInteractorIdentity,
             @NonNull ScheduledExecutorService scheduledExecutorService, boolean logging) {
         mRemoteHotwordDetectionService = remoteHotwordDetectionService;
         mLock = lock;
         mContext = context;
+        mToken = token;
         mCallback = callback;
         mVoiceInteractionServiceUid = voiceInteractionServiceUid;
         mVoiceInteractorIdentity = voiceInteractorIdentity;
@@ -238,7 +241,7 @@
                     try {
                         mCallback.onStatusReported(status);
                         HotwordMetricsLogger.writeServiceInitResultEvent(getDetectorType(),
-                                initResultMetricsResult);
+                                initResultMetricsResult, mVoiceInteractionServiceUid);
                     } catch (RemoteException e) {
                         Slog.w(TAG, "Failed to report initialization status: " + e);
                         HotwordMetricsLogger.writeDetectorEvent(getDetectorType(),
@@ -269,7 +272,7 @@
                 try {
                     mCallback.onStatusReported(INITIALIZATION_STATUS_UNKNOWN);
                     HotwordMetricsLogger.writeServiceInitResultEvent(getDetectorType(),
-                            METRICS_INIT_UNKNOWN_TIMEOUT);
+                            METRICS_INIT_UNKNOWN_TIMEOUT, mVoiceInteractionServiceUid);
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Failed to report initialization status UNKNOWN", e);
                     HotwordMetricsLogger.writeDetectorEvent(getDetectorType(),
@@ -474,8 +477,8 @@
                                             callback.onError();
                                             return;
                                         }
-                                        callback.onDetected(newResult, null /* audioFormat */,
-                                                null /* audioStream */);
+                                        callback.onDetected(newResult, /* audioFormat= */ null,
+                                                /* audioStream= */ null);
                                         Slog.i(TAG, "Egressed "
                                                 + HotwordDetectedResult.getUsageSize(newResult)
                                                 + " bits from hotword trusted process");
@@ -542,6 +545,30 @@
      */
     abstract void informRestartProcessLocked();
 
+    boolean isSameCallback(@Nullable IHotwordRecognitionStatusCallback callback) {
+        synchronized (mLock) {
+            if (callback == null) {
+                return false;
+            }
+            return mCallback.asBinder().equals(callback.asBinder());
+        }
+    }
+
+    boolean isSameToken(@NonNull IBinder token) {
+        synchronized (mLock) {
+            if (token == null) {
+                return false;
+            }
+            return mToken == token;
+        }
+    }
+
+    boolean isDestroyed() {
+        synchronized (mLock) {
+            return mDestroyed;
+        }
+    }
+
     private static Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() {
         ParcelFileDescriptor[] fileDescriptors;
         try {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
index 940aed3..61c18be 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
@@ -64,28 +64,28 @@
     /**
      * Logs information related to hotword detection service init result.
      */
-    public static void writeServiceInitResultEvent(int detectorType, int result) {
+    public static void writeServiceInitResultEvent(int detectorType, int result, int uid) {
         int metricsDetectorType = getInitMetricsDetectorType(detectorType);
         FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED,
-                metricsDetectorType, result);
+                metricsDetectorType, result, uid);
     }
 
     /**
      * Logs information related to hotword detection service restarting.
      */
-    public static void writeServiceRestartEvent(int detectorType, int reason) {
+    public static void writeServiceRestartEvent(int detectorType, int reason, int uid) {
         int metricsDetectorType = getRestartMetricsDetectorType(detectorType);
         FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED,
-                metricsDetectorType, reason);
+                metricsDetectorType, reason, uid);
     }
 
     /**
      * Logs information related to keyphrase trigger.
      */
-    public static void writeKeyphraseTriggerEvent(int detectorType, int result) {
+    public static void writeKeyphraseTriggerEvent(int detectorType, int result, int uid) {
         int metricsDetectorType = getKeyphraseMetricsDetectorType(detectorType);
         FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED,
-                metricsDetectorType, result);
+                metricsDetectorType, result, uid);
     }
 
     /**
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
index 6930689..4eb997a 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 import android.media.AudioFormat;
 import android.media.permission.Identity;
+import android.os.IBinder;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.SharedMemory;
@@ -35,8 +36,8 @@
 import android.service.voice.HotwordDetector;
 import android.service.voice.HotwordRejectedResult;
 import android.service.voice.IDspHotwordDetectionCallback;
-import android.service.voice.IHotwordDetectionService;
 import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
+import android.service.voice.ISandboxedDetectionService;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
@@ -63,12 +64,13 @@
 
     SoftwareTrustedHotwordDetectorSession(
             @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService,
-            @NonNull Object lock, @NonNull Context context,
+            @NonNull Object lock, @NonNull Context context, @NonNull IBinder token,
             @NonNull IHotwordRecognitionStatusCallback callback, int voiceInteractionServiceUid,
             Identity voiceInteractorIdentity,
             @NonNull ScheduledExecutorService scheduledExecutorService, boolean logging) {
-        super(remoteHotwordDetectionService, lock, context, callback, voiceInteractionServiceUid,
-                voiceInteractorIdentity, scheduledExecutorService, logging);
+        super(remoteHotwordDetectionService, lock, context, token, callback,
+                voiceInteractionServiceUid, voiceInteractorIdentity, scheduledExecutorService,
+                logging);
     }
 
     @SuppressWarnings("GuardedBy")
@@ -101,12 +103,14 @@
                 synchronized (mLock) {
                     HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                             HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
-                            HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED);
+                            HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED,
+                            mVoiceInteractionServiceUid);
                     if (!mPerformingSoftwareHotwordDetection) {
                         Slog.i(TAG, "Hotword detection has already completed");
                         HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
-                                METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK);
+                                METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK,
+                                mVoiceInteractionServiceUid);
                         return;
                     }
                     mPerformingSoftwareHotwordDetection = false;
@@ -115,7 +119,8 @@
                     } catch (SecurityException e) {
                         HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
-                                METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION);
+                                METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION,
+                                mVoiceInteractionServiceUid);
                         mSoftwareCallback.onError();
                         return;
                     }
@@ -144,7 +149,8 @@
                 }
                 HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                         HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
-                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED);
+                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED,
+                        mVoiceInteractionServiceUid);
                 // onRejected isn't allowed here, and we are not expecting it.
             }
         };
@@ -163,9 +169,9 @@
     }
 
     @SuppressWarnings("GuardedBy")
-    void stopListeningLocked() {
+    void stopListeningFromMicLocked() {
         if (DEBUG) {
-            Slog.d(TAG, "stopListeningLocked");
+            Slog.d(TAG, "stopListeningFromMicLocked");
         }
         if (!mPerformingSoftwareHotwordDetection) {
             Slog.i(TAG, "Hotword detection is not running");
@@ -173,7 +179,7 @@
         }
         mPerformingSoftwareHotwordDetection = false;
 
-        mRemoteHotwordDetectionService.run(IHotwordDetectionService::stopDetection);
+        mRemoteHotwordDetectionService.run(ISandboxedDetectionService::stopDetection);
 
         closeExternalAudioStreamLocked("stopping requested");
     }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 7207e373..9a02188 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -1245,14 +1245,15 @@
         @Override
         public void updateState(
                 @Nullable PersistableBundle options,
-                @Nullable SharedMemory sharedMemory) {
+                @Nullable SharedMemory sharedMemory,
+                @NonNull IBinder token) {
             super.updateState_enforcePermission();
 
             synchronized (this) {
                 enforceIsCurrentVoiceInteractionService();
 
                 Binder.withCleanCallingIdentity(
-                        () -> mImpl.updateStateLocked(options, sharedMemory));
+                        () -> mImpl.updateStateLocked(options, sharedMemory, token));
             }
         }
 
@@ -1262,6 +1263,7 @@
                 @NonNull Identity voiceInteractorIdentity,
                 @Nullable PersistableBundle options,
                 @Nullable SharedMemory sharedMemory,
+                @NonNull IBinder token,
                 IHotwordRecognitionStatusCallback callback,
                 int detectorType) {
             super.initAndVerifyDetector_enforcePermission();
@@ -1274,7 +1276,20 @@
 
                 Binder.withCleanCallingIdentity(
                         () -> mImpl.initAndVerifyDetectorLocked(voiceInteractorIdentity, options,
-                                sharedMemory, callback, detectorType));
+                                sharedMemory, token, callback, detectorType));
+            }
+        }
+
+        @Override
+        public void destroyDetector(@NonNull IBinder token) {
+            synchronized (this) {
+                if (mImpl == null) {
+                    Slog.w(TAG, "destroyDetector without running voice interaction service");
+                    return;
+                }
+
+                Binder.withCleanCallingIdentity(
+                        () -> mImpl.destroyDetectorLocked(token));
             }
         }
 
@@ -1326,6 +1341,7 @@
                 ParcelFileDescriptor audioStream,
                 AudioFormat audioFormat,
                 PersistableBundle options,
+                @NonNull IBinder token,
                 IMicrophoneHotwordDetectionVoiceInteractionCallback callback)
                 throws RemoteException {
             synchronized (this) {
@@ -1338,8 +1354,8 @@
                 }
                 final long caller = Binder.clearCallingIdentity();
                 try {
-                    mImpl.startListeningFromExternalSourceLocked(
-                            audioStream, audioFormat, options, callback);
+                    mImpl.startListeningFromExternalSourceLocked(audioStream, audioFormat, options,
+                            token, callback);
                 } finally {
                     Binder.restoreCallingIdentity(caller);
                 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 0a660b0..f041adc 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -56,7 +56,6 @@
 import android.os.ServiceManager;
 import android.os.SharedMemory;
 import android.os.UserHandle;
-import android.service.voice.HotwordDetector;
 import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
 import android.service.voice.IVoiceInteractionService;
 import android.service.voice.IVoiceInteractionSession;
@@ -113,7 +112,6 @@
 
     VoiceInteractionSessionConnection mActiveSession;
     int mDisabledShowContext;
-    int mDetectorType;
 
     final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
@@ -552,7 +550,8 @@
 
     public void updateStateLocked(
             @Nullable PersistableBundle options,
-            @Nullable SharedMemory sharedMemory) {
+            @Nullable SharedMemory sharedMemory,
+            @NonNull IBinder token) {
         Slog.v(TAG, "updateStateLocked");
 
         if (sharedMemory != null && !sharedMemory.setProtect(OsConstants.PROT_READ)) {
@@ -565,7 +564,7 @@
             throw new IllegalStateException("Hotword detection connection not found");
         }
         synchronized (mHotwordDetectionConnection.mLock) {
-            mHotwordDetectionConnection.updateStateLocked(options, sharedMemory);
+            mHotwordDetectionConnection.updateStateLocked(options, sharedMemory, token);
         }
     }
 
@@ -573,6 +572,7 @@
             @NonNull Identity voiceInteractorIdentity,
             @Nullable PersistableBundle options,
             @Nullable SharedMemory sharedMemory,
+            @NonNull IBinder token,
             IHotwordRecognitionStatusCallback callback,
             int detectorType) {
         Slog.v(TAG, "initAndVerifyDetectorLocked");
@@ -624,16 +624,26 @@
             throw new IllegalStateException("Can't set sharedMemory to be read-only");
         }
 
-        mDetectorType = detectorType;
-
         logDetectorCreateEventIfNeeded(callback, detectorType, true,
                 voiceInteractionServiceUid);
         if (mHotwordDetectionConnection == null) {
             mHotwordDetectionConnection = new HotwordDetectionConnection(mServiceStub, mContext,
                     mInfo.getServiceInfo().applicationInfo.uid, voiceInteractorIdentity,
                     mHotwordDetectionComponentName, mUser, /* bindInstantServiceAllowed= */ false,
-                    options, sharedMemory, callback, detectorType);
+                    detectorType);
         }
+        mHotwordDetectionConnection.createDetectorLocked(options, sharedMemory, token, callback,
+                detectorType);
+    }
+
+    public void destroyDetectorLocked(IBinder token) {
+        Slog.v(TAG, "destroyDetectorLocked");
+
+        if (mHotwordDetectionConnection == null) {
+            Slog.w(TAG, "destroy detector callback, but no hotword detection connection");
+            return;
+        }
+        mHotwordDetectionConnection.destroyDetectorLocked(token);
     }
 
     private void logDetectorCreateEventIfNeeded(IHotwordRecognitionStatusCallback callback,
@@ -642,19 +652,16 @@
             HotwordMetricsLogger.writeDetectorCreateEvent(detectorType, isCreated,
                     voiceInteractionServiceUid);
         }
-
     }
 
     public void shutdownHotwordDetectionServiceLocked() {
         if (DEBUG) {
             Slog.d(TAG, "shutdownHotwordDetectionServiceLocked");
         }
-
         if (mHotwordDetectionConnection == null) {
             Slog.w(TAG, "shutdown, but no hotword detection connection");
             return;
         }
-
         mHotwordDetectionConnection.cancelLocked();
         mHotwordDetectionConnection = null;
     }
@@ -663,7 +670,7 @@
             AudioFormat audioFormat,
             IMicrophoneHotwordDetectionVoiceInteractionCallback callback) {
         if (DEBUG) {
-            Slog.d(TAG, "startListeningFromMic");
+            Slog.d(TAG, "startListeningFromMicLocked");
         }
 
         if (mHotwordDetectionConnection == null) {
@@ -671,16 +678,17 @@
             return;
         }
 
-        mHotwordDetectionConnection.startListeningFromMic(audioFormat, callback);
+        mHotwordDetectionConnection.startListeningFromMicLocked(audioFormat, callback);
     }
 
     public void startListeningFromExternalSourceLocked(
             ParcelFileDescriptor audioStream,
             AudioFormat audioFormat,
             @Nullable PersistableBundle options,
+            @NonNull IBinder token,
             IMicrophoneHotwordDetectionVoiceInteractionCallback callback) {
         if (DEBUG) {
-            Slog.d(TAG, "startListeningFromExternalSource");
+            Slog.d(TAG, "startListeningFromExternalSourceLocked");
         }
 
         if (mHotwordDetectionConnection == null) {
@@ -693,21 +701,21 @@
             throw new IllegalStateException("External source is null for hotword detector");
         }
 
-        mHotwordDetectionConnection
-                .startListeningFromExternalSource(audioStream, audioFormat, options, callback);
+        mHotwordDetectionConnection.startListeningFromExternalSourceLocked(audioStream, audioFormat,
+                options, token, callback);
     }
 
     public void stopListeningFromMicLocked() {
         if (DEBUG) {
-            Slog.d(TAG, "stopListeningFromMic");
+            Slog.d(TAG, "stopListeningFromMicLocked");
         }
 
         if (mHotwordDetectionConnection == null) {
-            Slog.w(TAG, "stopListeningFromMic() called but connection isn't established");
+            Slog.w(TAG, "stopListeningFromMicLocked() called but connection isn't established");
             return;
         }
 
-        mHotwordDetectionConnection.stopListening();
+        mHotwordDetectionConnection.stopListeningFromMicLocked();
     }
 
     public void triggerHardwareRecognitionEventForTestLocked(
@@ -730,7 +738,7 @@
             Slog.d(TAG, "createSoundTriggerCallbackLocked");
         }
         return new HotwordDetectionConnection.SoundTriggerCallback(callback,
-                mHotwordDetectionConnection);
+                mHotwordDetectionConnection, mInfo.getServiceInfo().applicationInfo.uid);
     }
 
     private static ServiceInfo getServiceInfoLocked(@NonNull ComponentName componentName,
@@ -809,8 +817,6 @@
             pw.println(Integer.toHexString(mDisabledShowContext));
         }
         pw.print("  mBound="); pw.print(mBound);  pw.print(" mService="); pw.println(mService);
-        pw.print("  mDetectorType=");
-        pw.println(HotwordDetector.detectorTypeToString(mDetectorType));
         if (mHotwordDetectionConnection != null) {
             pw.println("  Hotword detection connection:");
             mHotwordDetectionConnection.dump("    ", pw);
@@ -899,5 +905,8 @@
     @Override
     public void onSessionHidden(VoiceInteractionSessionConnection connection) {
         mServiceStub.onSessionHidden();
+        // Notifies visibility change here can cause duplicate events, it is added to make sure
+        // client always get the callback even if session is unexpectedly closed.
+        mServiceStub.setSessionWindowVisible(connection.mToken, false);
     }
 }
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 22cd31a..c4744ef 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -17,6 +17,7 @@
 package android.telephony;
 
 import android.Manifest;
+import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -52,6 +53,7 @@
 import com.android.telephony.Rlog;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
@@ -9957,10 +9959,13 @@
      * @param subId the subscription ID, normally obtained from {@link SubscriptionManager}.
      * @return A {@link PersistableBundle} containing the config for the given subId, or default
      *         values for an invalid subId.
+     *
+     * @deprecated Use {@link #getConfigForSubId(int, String...)} instead.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
     @Nullable
+    @Deprecated
     public PersistableBundle getConfigForSubId(int subId) {
         try {
             ICarrierConfigLoader loader = getICarrierConfigLoader();
@@ -9979,6 +9984,59 @@
     }
 
     /**
+     * Gets the configuration values of the specified keys for a particular subscription.
+     *
+     * <p>If an invalid subId is used, the returned configuration will contain default values for
+     * the specified keys. If the value for the key can't be found, the returned configuration will
+     * filter the key out.
+     *
+     * <p>After using this method to get the configuration bundle,
+     * {@link #isConfigForIdentifiedCarrier(PersistableBundle)} should be called to confirm whether
+     * any carrier specific configuration has been applied.
+     *
+     * <p>Note that on success, the key/value for {@link #KEY_CARRIER_CONFIG_VERSION_STRING} and
+     * {@link #KEY_CARRIER_CONFIG_APPLIED_BOOL} are always in the returned bundle, no matter if they
+     * were explicitly requested.
+     *
+     * <p>Requires Permission:
+     * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}, or the calling app
+     * has carrier privileges on the specified subscription (see
+     * {@link TelephonyManager#hasCarrierPrivileges()}).
+     *
+     * @param subId The subscription ID on which the carrier config should be retrieved.
+     * @param keys  The carrier config keys to retrieve values.
+     * @return A {@link PersistableBundle} with key/value mapping for the specified configuration
+     * on success, or an empty (but never null) bundle on failure (for example, when the calling app
+     * has no permission).
+     */
+    @RequiresPermission(anyOf = {
+            Manifest.permission.READ_PHONE_STATE,
+            "carrier privileges",
+    })
+    @NonNull
+    public PersistableBundle getConfigForSubId(int subId, @NonNull String... keys) {
+        Objects.requireNonNull(keys, "Config keys should be non-null");
+        for (String key : keys) {
+            Objects.requireNonNull(key, "Config key should be non-null");
+        }
+
+        try {
+            ICarrierConfigLoader loader = getICarrierConfigLoader();
+            if (loader == null) {
+                Rlog.w(TAG, "Error getting config for subId " + subId
+                        + " ICarrierConfigLoader is null");
+                throw new IllegalStateException("Carrier config loader is not available.");
+            }
+            return loader.getConfigSubsetForSubIdWithFeature(subId, mContext.getOpPackageName(),
+                    mContext.getAttributionTag(), keys);
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "Error getting config for subId " + subId + ": " + ex);
+            ex.rethrowAsRuntimeException();
+        }
+        return new PersistableBundle();
+    }
+
+    /**
      * Overrides the carrier config of the provided subscription ID with the provided values.
      *
      * Any further queries to carrier config from any process will return the overridden values
@@ -10052,15 +10110,52 @@
      * has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges()}).
      *
      * @see #getConfigForSubId
+     * @see #getConfig(String...)
+     * @deprecated use {@link #getConfig(String...)} instead.
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
     @Nullable
+    @Deprecated
     public PersistableBundle getConfig() {
         return getConfigForSubId(SubscriptionManager.getDefaultSubscriptionId());
     }
 
     /**
+     * Gets the configuration values of the specified config keys applied for the default
+     * subscription.
+     *
+     * <p>If the value for the key can't be found, the returned bundle will filter the key out.
+     *
+     * <p>After using this method to get the configuration bundle, {@link
+     * #isConfigForIdentifiedCarrier(PersistableBundle)} should be called to confirm whether any
+     * carrier specific configuration has been applied.
+     *
+     * <p>Note that on success, the key/value for {@link #KEY_CARRIER_CONFIG_VERSION_STRING} and
+     * {@link #KEY_CARRIER_CONFIG_APPLIED_BOOL} are always in the returned bundle, no matter if
+     * they were explicitly requested.
+     *
+     * <p>Requires Permission:
+     * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}, or the calling app
+     * has carrier privileges for the default subscription (see
+     * {@link TelephonyManager#hasCarrierPrivileges()}).
+     *
+     * @param keys The config keys to retrieve values
+     * @return A {@link PersistableBundle} with key/value mapping for the specified carrier
+     * configs on success, or an empty (but never null) bundle on failure.
+     * @see #getConfigForSubId(int, String...)
+     * @see SubscriptionManager#getDefaultSubscriptionId()
+     */
+    @RequiresPermission(anyOf = {
+            Manifest.permission.READ_PHONE_STATE,
+            "carrier privileges",
+    })
+    @NonNull
+    public PersistableBundle getConfig(@NonNull String... keys) {
+        return getConfigForSubId(SubscriptionManager.getDefaultSubscriptionId(), keys);
+    }
+
+    /**
      * Determines whether a configuration {@link PersistableBundle} obtained from
      * {@link #getConfig()} or {@link #getConfigForSubId(int)} corresponds to an identified carrier.
      *
@@ -10246,4 +10341,85 @@
             configs.putPersistableBundle(key, (PersistableBundle) value);
         }
     }
+
+    /**
+     * Listener interface to get a notification when the carrier configurations have changed.
+     *
+     * Use this listener to receive timely updates when the carrier configuration changes. System
+     * components should prefer this listener over {@link #ACTION_CARRIER_CONFIG_CHANGED}
+     * whenever possible.
+     *
+     * To register the listener, call
+     * {@link #registerCarrierConfigChangeListener(Executor, CarrierConfigChangeListener)}.
+     * To unregister, call
+     * {@link #unregisterCarrierConfigChangeListener(CarrierConfigChangeListener)}.
+     *
+     * Note that on registration, registrants will NOT receive a notification on last carrier config
+     * change. Only carrier configs change AFTER the registration will be sent to registrants. And
+     * unlike {@link #ACTION_CARRIER_CONFIG_CHANGED}, notification wouldn't send when the device is
+     * unlocked. Registrants only receive the notification when there has been real carrier config
+     * changes.
+     *
+     * @see #registerCarrierConfigChangeListener(Executor, CarrierConfigChangeListener)
+     * @see #unregisterCarrierConfigChangeListener(CarrierConfigChangeListener)
+     * @see #ACTION_CARRIER_CONFIG_CHANGED
+     * @see #getConfig(String...)
+     * @see #getConfigForSubId(int, String...)
+     */
+    public interface CarrierConfigChangeListener {
+        /**
+         * Called when carrier configurations have changed.
+         *
+         * @param logicalSlotIndex  The logical SIM slot index on which to monitor and get
+         *                          notification. It is guaranteed to be valid.
+         * @param subscriptionId    The subscription on the SIM slot. May be
+         *                          {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}.
+         * @param carrierId         The optional carrier Id, may be
+         *                          {@link TelephonyManager#UNKNOWN_CARRIER_ID}.
+         *                          See {@link TelephonyManager#getSimCarrierId()}.
+         * @param specificCarrierId The optional fine-grained carrierId, may be {@link
+         *                          TelephonyManager#UNKNOWN_CARRIER_ID}. A specific carrierId may
+         *                          be different from the carrierId above in a MVNO scenario. See
+         *                          detail in {@link TelephonyManager#getSimSpecificCarrierId()}.
+         */
+        void onCarrierConfigChanged(int logicalSlotIndex, int subscriptionId, int carrierId,
+                int specificCarrierId);
+    }
+
+    /**
+     * Register a {@link CarrierConfigChangeListener} to get a notification when carrier
+     * configurations have changed.
+     *
+     * @param executor The executor on which the listener will be called.
+     * @param listener The CarrierConfigChangeListener called when carrier configs has changed.
+     */
+    public void registerCarrierConfigChangeListener(@NonNull @CallbackExecutor Executor executor,
+            @NonNull CarrierConfigChangeListener listener) {
+        Objects.requireNonNull(executor, "Executor should be non-null.");
+        Objects.requireNonNull(listener, "Listener should be non-null.");
+
+        TelephonyRegistryManager trm = mContext.getSystemService(TelephonyRegistryManager.class);
+        if (trm == null) {
+            throw new IllegalStateException("Telephony registry service is null");
+        }
+        trm.addCarrierConfigChangedListener(executor, listener);
+    }
+
+    /**
+     * Unregister the {@link CarrierConfigChangeListener} to stop notification on carrier
+     * configurations change.
+     *
+     * @param listener The CarrierConfigChangeListener which was registered with method
+     * {@link #registerCarrierConfigChangeListener(Executor, CarrierConfigChangeListener)}.
+     */
+    public void unregisterCarrierConfigChangeListener(
+            @NonNull CarrierConfigChangeListener listener) {
+        Objects.requireNonNull(listener, "Listener should be non-null.");
+
+        TelephonyRegistryManager trm = mContext.getSystemService(TelephonyRegistryManager.class);
+        if (trm == null) {
+            throw new IllegalStateException("Telephony registry service is null");
+        }
+        trm.removeCarrierConfigChangedListener(listener);
+    }
 }
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 9b566fb..8e8755d 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -58,6 +58,7 @@
 import android.provider.Telephony.SimInfo;
 import android.telephony.euicc.EuiccManager;
 import android.telephony.ims.ImsMmTelManager;
+import android.text.TextUtils;
 import android.util.Base64;
 import android.util.Log;
 import android.util.Pair;
@@ -370,7 +371,7 @@
 
     /**
      * A content {@link Uri} used to receive updates on advanced calling user setting
-     * @see ImsMmTelManager#isAdvancedCallingSettingEnabled().
+     *
      * <p>
      * Use this {@link Uri} with a {@link ContentObserver} to be notified of changes to the
      * subscription advanced calling enabled
@@ -381,6 +382,9 @@
      * delivery of updates to the {@link Uri}.
      * To be notified of changes to a specific subId, append subId to the URI
      * {@link Uri#withAppendedPath(Uri, String)}.
+     *
+     * @see ImsMmTelManager#isAdvancedCallingSettingEnabled()
+     *
      * @hide
      */
     @NonNull
@@ -1165,7 +1169,7 @@
      *
      * An opportunistic subscription will default to data-centric.
      *
-     * {@see SubscriptionInfo#isOpportunistic}
+     * @see SubscriptionInfo#isOpportunistic
      */
     public static final int USAGE_SETTING_DEFAULT = 0;
 
@@ -1949,7 +1953,7 @@
      *
      * <p>Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission.
      *
-     * @see {@link TelephonyManager#getCardIdForDefaultEuicc()} for more information on the card ID.
+     * @see TelephonyManager#getCardIdForDefaultEuicc() for more information on the card ID.
      *
      * @hide
      */
@@ -1979,7 +1983,7 @@
      *
      * @param cardId the card ID of the eUICC.
      *
-     * @see {@link TelephonyManager#getCardIdForDefaultEuicc()} for more information on the card ID.
+     * @see TelephonyManager#getCardIdForDefaultEuicc() for more information on the card ID.
      *
      * @hide
      */
@@ -2103,10 +2107,15 @@
     }
 
     /**
-     * Remove SubscriptionInfo record from the SubscriptionInfo database
+     * Remove subscription info record from the subscription database.
+     *
      * @param uniqueId This is the unique identifier for the subscription within the specific
-     *                 subscription type.
-     * @param subscriptionType the {@link #SUBSCRIPTION_TYPE}
+     * subscription type.
+     * @param subscriptionType the type of subscription to be removed.
+     *
+     * @throws NullPointerException if {@code uniqueId} is {@code null}.
+     * @throws SecurityException if callers do not hold the required permission.
+     *
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -2457,20 +2466,6 @@
         return getActiveSubscriptionInfo(getDefaultDataSubscriptionId());
     }
 
-    /** @hide */
-    public void clearSubscriptionInfo() {
-        try {
-            ISub iSub = TelephonyManager.getSubscriptionService();
-            if (iSub != null) {
-                iSub.clearSubInfo();
-            }
-        } catch (RemoteException ex) {
-            // ignore it
-        }
-
-        return;
-    }
-
     /**
      * Check if the supplied subscription ID is valid.
      *
@@ -2614,17 +2609,27 @@
     }
 
     /**
-     * Store properties associated with SubscriptionInfo in database
-     * @param subId Subscription Id of Subscription
-     * @param propKey Column name in database associated with SubscriptionInfo
-     * @param propValue Value to store in DB for particular subId & column name
+     * Set a field in the subscription database. Note not all fields are supported.
+     *
+     * @param subscriptionId Subscription Id of Subscription.
+     * @param columnName Column name in the database. Note not all fields are supported.
+     * @param value Value to store in the database.
+     *
+     * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not
+     * exposed.
+     * @throws SecurityException if callers do not hold the required permission.
+     *
+     * @see android.provider.Telephony.SimInfo for all the columns.
+     *
      * @hide
      */
-    public static void setSubscriptionProperty(int subId, String propKey, String propValue) {
+    @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
+    public static void setSubscriptionProperty(int subscriptionId, @NonNull String columnName,
+            @NonNull String value) {
         try {
             ISub iSub = TelephonyManager.getSubscriptionService();
             if (iSub != null) {
-                iSub.setSubscriptionProperty(subId, propKey, propValue);
+                iSub.setSubscriptionProperty(subscriptionId, columnName, value);
             }
         } catch (RemoteException ex) {
             // ignore it
@@ -2653,118 +2658,149 @@
     }
 
     /**
-     * Return list of contacts uri corresponding to query result.
-     * @param subId Subscription Id of Subscription
-     * @param propKey Column name in SubscriptionInfo database
-     * @return list of contacts uri to be returned
+     * Get specific field in string format from the subscription info database.
+     *
+     * @param context The calling context.
+     * @param subscriptionId Subscription id of the subscription.
+     * @param columnName Column name in subscription database.
+     *
+     * @return Value in string format associated with {@code subscriptionId} and {@code columnName}
+     * from the database.
+     *
+     * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not
+     * exposed.
+     *
+     * @see android.provider.Telephony.SimInfo for all the columns.
+     *
      * @hide
      */
-    private static List<Uri> getContactsFromSubscriptionProperty(int subId, String propKey,
-            Context context) {
-        String result = getSubscriptionProperty(subId, propKey, context);
-        if (result != null) {
-            try {
-                byte[] b = Base64.decode(result, Base64.DEFAULT);
-                ByteArrayInputStream bis = new ByteArrayInputStream(b);
-                ObjectInputStream ois = new ObjectInputStream(bis);
-                List<String> contacts = ArrayList.class.cast(ois.readObject());
-                List<Uri> uris = new ArrayList<>();
-                for (String contact : contacts) {
-                    uris.add(Uri.parse(contact));
-                }
-                return uris;
-            } catch (IOException e) {
-                logd("getContactsFromSubscriptionProperty IO exception");
-            } catch (ClassNotFoundException e) {
-                logd("getContactsFromSubscriptionProperty ClassNotFound exception");
-            }
-        }
-        return new ArrayList<>();
-    }
-
-    /**
-     * Store properties associated with SubscriptionInfo in database
-     * @param subId Subscription Id of Subscription
-     * @param propKey Column name in SubscriptionInfo database
-     * @return Value associated with subId and propKey column in database
-     * @hide
-     */
-    private static String getSubscriptionProperty(int subId, String propKey,
-            Context context) {
+    @NonNull
+    @RequiresPermission(anyOf = {
+            Manifest.permission.READ_PHONE_STATE,
+            Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            "carrier privileges",
+    })
+    private static String getStringSubscriptionProperty(@NonNull Context context,
+            int subscriptionId, @NonNull String columnName) {
         String resultValue = null;
         try {
             ISub iSub = TelephonyManager.getSubscriptionService();
             if (iSub != null) {
-                resultValue = iSub.getSubscriptionProperty(subId, propKey,
+                resultValue = iSub.getSubscriptionProperty(subscriptionId, columnName,
                         context.getOpPackageName(), context.getAttributionTag());
             }
         } catch (RemoteException ex) {
             // ignore it
         }
-        return resultValue;
+        return TextUtils.emptyIfNull(resultValue);
     }
 
     /**
-     * Returns boolean value corresponding to query result.
-     * @param subId Subscription Id of Subscription
-     * @param propKey Column name in SubscriptionInfo database
-     * @param defValue Default boolean value to be returned
-     * @return boolean result value to be returned
+     * Get specific field in {@code boolean} format from the subscription info database.
+     *
+     * @param subscriptionId Subscription id of the subscription.
+     * @param columnName Column name in subscription database.
+     * @param defaultValue Default value in case not found or error.
+     * @param context The calling context.
+     *
+     * @return Value in {@code boolean} format associated with {@code subscriptionId} and
+     * {@code columnName} from the database, or {@code defaultValue} if not found or error.
+     *
+     * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not
+     * exposed.
+     *
+     * @see android.provider.Telephony.SimInfo for all the columns.
+     *
      * @hide
      */
-    public static boolean getBooleanSubscriptionProperty(int subId, String propKey,
-            boolean defValue, Context context) {
-        String result = getSubscriptionProperty(subId, propKey, context);
-        if (result != null) {
+    @RequiresPermission(anyOf = {
+            Manifest.permission.READ_PHONE_STATE,
+            Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            "carrier privileges",
+    })
+    public static boolean getBooleanSubscriptionProperty(int subscriptionId,
+            @NonNull String columnName, boolean defaultValue, @NonNull Context context) {
+        String result = getStringSubscriptionProperty(context, subscriptionId, columnName);
+        if (!result.isEmpty()) {
             try {
                 return Integer.parseInt(result) == 1;
             } catch (NumberFormatException err) {
                 logd("getBooleanSubscriptionProperty NumberFormat exception");
             }
         }
-        return defValue;
+        return defaultValue;
     }
 
     /**
-     * Returns integer value corresponding to query result.
-     * @param subId Subscription Id of Subscription
-     * @param propKey Column name in SubscriptionInfo database
-     * @param defValue Default integer value to be returned
-     * @return integer result value to be returned
+     * Get specific field in {@code integer} format from the subscription info database.
+     *
+     * @param subscriptionId Subscription id of the subscription.
+     * @param columnName Column name in subscription database.
+     * @param defaultValue Default value in case not found or error.
+     * @param context The calling context.
+     *
+     * @return Value in {@code integer} format associated with {@code subscriptionId} and
+     * {@code columnName} from the database, or {@code defaultValue} if not found or error.
+     *
+     * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not
+     * exposed.
+     *
+     * @see android.provider.Telephony.SimInfo for all the columns.
+     *
      * @hide
      */
-    public static int getIntegerSubscriptionProperty(int subId, String propKey, int defValue,
-            Context context) {
-        String result = getSubscriptionProperty(subId, propKey, context);
-        if (result != null) {
+    @RequiresPermission(anyOf = {
+            Manifest.permission.READ_PHONE_STATE,
+            Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            "carrier privileges",
+    })
+    public static int getIntegerSubscriptionProperty(int subscriptionId, @NonNull String columnName,
+            int defaultValue, @NonNull Context context) {
+        String result = getStringSubscriptionProperty(context, subscriptionId, columnName);
+        if (!result.isEmpty()) {
             try {
                 return Integer.parseInt(result);
             } catch (NumberFormatException err) {
                 logd("getIntegerSubscriptionProperty NumberFormat exception");
             }
         }
-        return defValue;
+        return defaultValue;
     }
 
     /**
-     * Returns long value corresponding to query result.
-     * @param subId Subscription Id of Subscription
-     * @param propKey Column name in SubscriptionInfo database
-     * @param defValue Default long value to be returned
-     * @return long result value to be returned
+     * Get specific field in {@code long} format from the subscription info database.
+     *
+     * @param subscriptionId Subscription id of the subscription.
+     * @param columnName Column name in subscription database.
+     * @param defaultValue Default value in case not found or error.
+     * @param context The calling context.
+     *
+     * @return Value in {@code long} format associated with {@code subscriptionId} and
+     * {@code columnName} from the database, or {@code defaultValue} if not found or error.
+     *
+     * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not
+     * exposed.
+     *
+     * @see android.provider.Telephony.SimInfo for all the columns.
+     *
      * @hide
      */
-    public static long getLongSubscriptionProperty(int subId, String propKey, long defValue,
-                                                     Context context) {
-        String result = getSubscriptionProperty(subId, propKey, context);
-        if (result != null) {
+    @RequiresPermission(anyOf = {
+            Manifest.permission.READ_PHONE_STATE,
+            Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            "carrier privileges",
+    })
+    public static long getLongSubscriptionProperty(int subscriptionId, @NonNull String columnName,
+            long defaultValue, @NonNull Context context) {
+        String result = getStringSubscriptionProperty(context, subscriptionId, columnName);
+        if (!result.isEmpty()) {
             try {
                 return Long.parseLong(result);
             } catch (NumberFormatException err) {
                 logd("getLongSubscriptionProperty NumberFormat exception");
             }
         }
-        return defValue;
+        return defaultValue;
     }
 
     /**
@@ -3002,7 +3038,6 @@
      *            considered unmetered.
      * @param networkTypes the network types this override applies to. If no
      *            network types are specified, override values will be ignored.
-     *            {@see TelephonyManager#getAllNetworkTypes()}
      * @param expirationDurationMillis the duration after which the requested override
      *            will be automatically cleared, or {@code 0} to leave in the
      *            requested state until explicitly cleared, or the next reboot,
@@ -3063,17 +3098,14 @@
      * </ul>
      *
      * @param subId the subscriber this override applies to.
-     * @param overrideCongested set if the subscription should be considered
-     *            congested.
-     * @param networkTypes the network types this override applies to. If no
-     *            network types are specified, override values will be ignored.
-     *            {@see TelephonyManager#getAllNetworkTypes()}
+     * @param overrideCongested set if the subscription should be considered congested.
+     * @param networkTypes the network types this override applies to. If no network types are
+     * specified, override values will be ignored.
      * @param expirationDurationMillis the duration after which the requested override
-     *            will be automatically cleared, or {@code 0} to leave in the
-     *            requested state until explicitly cleared, or the next reboot,
-     *            whichever happens first.
-     * @throws SecurityException if the caller doesn't meet the requirements
-     *            outlined above.
+     * will be automatically cleared, or {@code 0} to leave in the requested state until explicitly
+     * cleared, or the next reboot, whichever happens first.
+     *
+     * @throws SecurityException if the caller doesn't meet the requirements outlined above.
      */
     public void setSubscriptionOverrideCongested(int subId, boolean overrideCongested,
             @NonNull @Annotation.NetworkType int[] networkTypes,
@@ -3089,10 +3121,11 @@
      *
      * Only supported for embedded subscriptions (if {@link SubscriptionInfo#isEmbedded} returns
      * true). To check for permissions for non-embedded subscription as well,
-     * {@see android.telephony.TelephonyManager#hasCarrierPrivileges}.
      *
      * @param info The subscription to check.
      * @return whether the app is authorized to manage this subscription per its metadata.
+     *
+     * @see android.telephony.TelephonyManager#hasCarrierPrivileges
      */
     public boolean canManageSubscription(SubscriptionInfo info) {
         return canManageSubscription(info, mContext.getPackageName());
@@ -3105,11 +3138,13 @@
      *
      * Only supported for embedded subscriptions (if {@link SubscriptionInfo#isEmbedded} returns
      * true). To check for permissions for non-embedded subscription as well,
-     * {@see android.telephony.TelephonyManager#hasCarrierPrivileges}.
      *
      * @param info The subscription to check.
      * @param packageName Package name of the app to check.
+     *
      * @return whether the app is authorized to manage this subscription per its access rules.
+     *
+     * @see android.telephony.TelephonyManager#hasCarrierPrivileges
      * @hide
      */
     @SystemApi
@@ -3423,21 +3458,20 @@
 
     /**
      * Remove a list of subscriptions from their subscription group.
-     * See {@link #createSubscriptionGroup(List)} for more details.
      *
      * Caller will either have {@link android.Manifest.permission#MODIFY_PHONE_STATE}
-     * permission or had carrier privilege permission on the subscriptions:
-     * {@link TelephonyManager#hasCarrierPrivileges()} or
-     * {@link #canManageSubscription(SubscriptionInfo)}
-     *
-     * @throws SecurityException if the caller doesn't meet the requirements
-     *             outlined above.
-     * @throws IllegalArgumentException if the some subscriptions in the list doesn't belong
-     *             the specified group.
-     * @throws IllegalStateException if Telephony service is in bad state.
+     * permission or has carrier privilege permission on all of the subscriptions provided in
+     * {@code subIdList}.
      *
      * @param subIdList list of subId that need removing from their groups.
+     * @param groupUuid The UUID of the subscription group.
      *
+     * @throws SecurityException if the caller doesn't meet the requirements outlined above.
+     * @throws IllegalArgumentException if the some subscriptions in the list doesn't belong the
+     * specified group.
+     * @throws IllegalStateException if Telephony service is in bad state.
+     *
+     * @see #createSubscriptionGroup(List)
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
@@ -3445,7 +3479,7 @@
             @NonNull ParcelUuid groupUuid) {
         Preconditions.checkNotNull(subIdList, "subIdList can't be null.");
         Preconditions.checkNotNull(groupUuid, "groupUuid can't be null.");
-        String pkgForDebug = mContext != null ? mContext.getOpPackageName() : "<unknown>";
+        String callingPackage = mContext != null ? mContext.getOpPackageName() : "<unknown>";
         if (VDBG) {
             logd("[removeSubscriptionsFromGroup]");
         }
@@ -3455,7 +3489,7 @@
         try {
             ISub iSub = TelephonyManager.getSubscriptionService();
             if (iSub != null) {
-                iSub.removeSubscriptionsFromGroup(subIdArray, groupUuid, pkgForDebug);
+                iSub.removeSubscriptionsFromGroup(subIdArray, groupUuid, callingPackage);
             } else {
                 if (!isSystemProcess()) {
                     throw new IllegalStateException("telephony service is null.");
@@ -3493,7 +3527,6 @@
      * @param groupUuid of which list of subInfo will be returned.
      * @return list of subscriptionInfo that belong to the same group, including the given
      * subscription itself. It will return an empty list if no subscription belongs to the group.
-     *
      */
     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
     @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
@@ -3533,7 +3566,8 @@
      * want to see their own hidden subscriptions.
      *
      * @param info the subscriptionInfo to check against.
-     * @return true if this subscription should be visible to the API caller.
+     *
+     * @return {@code true} if this subscription should be visible to the API caller.
      *
      * @hide
      */
@@ -3606,9 +3640,9 @@
      * <p>
      * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required
      *
+     * @param subscriptionId Subscription to be enabled or disabled. It could be a eSIM or pSIM
+     * subscription.
      * @param enable whether user is turning it on or off.
-     * @param subscriptionId Subscription to be enabled or disabled.
-     *                       It could be a eSIM or pSIM subscription.
      *
      * @return whether the operation is successful.
      *
@@ -3641,8 +3675,6 @@
      * available from SubscriptionInfo.areUiccApplicationsEnabled() will be updated
      * immediately.)
      *
-     * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required
-     *
      * @param subscriptionId which subscription to operate on.
      * @param enabled whether uicc applications are enabled or disabled.
      * @hide
@@ -3675,8 +3707,6 @@
      * It provides whether a physical SIM card can be disabled without taking it out, which is done
      * via {@link #setSubscriptionEnabled(int, boolean)} API.
      *
-     * Requires Permission: READ_PRIVILEGED_PHONE_STATE.
-     *
      * @return whether can disable subscriptions on physical SIMs.
      *
      * @hide
@@ -3704,13 +3734,9 @@
     }
 
     /**
-     * Check if a subscription is active.
+     * Check if the subscription is currently active in any slot.
      *
-     * @param subscriptionId The subscription id to check.
-     *
-     * @return {@code true} if the subscription is active.
-     *
-     * @throws IllegalArgumentException if the provided slot index is invalid.
+     * @param subscriptionId The subscription id.
      *
      * @hide
      */
@@ -3730,15 +3756,14 @@
     }
 
     /**
-     * Set the device to device status sharing user preference for a subscription ID. The setting
+     * Set the device to device status sharing user preference for a subscription id. The setting
      * app uses this method to indicate with whom they wish to share device to device status
      * information.
      *
-     * @param subscriptionId the unique Subscription ID in database.
-     * @param sharing the status sharing preference.
+     * @param subscriptionId The subscription id.
+     * @param sharing The status sharing preference.
      *
-     * @throws IllegalArgumentException if the subscription does not exist, or the sharing
-     * preference is invalid.
+     * @throws SecurityException if the caller doesn't have permissions required.
      */
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
     public void setDeviceToDeviceStatusSharingPreference(int subscriptionId,
@@ -3755,6 +3780,8 @@
      * Returns the user-chosen device to device status sharing preference
      * @param subscriptionId Subscription id of subscription
      * @return The device to device status sharing preference
+     *
+     * @throws SecurityException if the caller doesn't have permissions required.
      */
     public @DeviceToDeviceStatusSharingPreference int getDeviceToDeviceStatusSharingPreference(
             int subscriptionId) {
@@ -3766,15 +3793,14 @@
     }
 
     /**
-     * Set the list of contacts that allow device to device status sharing for a subscription ID.
+     * Set the list of contacts that allow device to device status sharing for a subscription id.
      * The setting app uses this method to indicate with whom they wish to share device to device
      * status information.
      *
-     * @param subscriptionId The unique Subscription ID in database.
+     * @param subscriptionId The subscription id.
      * @param contacts The list of contacts that allow device to device status sharing.
      *
-     * @throws IllegalArgumentException if the subscription does not exist, or contacts is
-     * {@code null}.
+     * @throws SecurityException if the caller doesn't have permissions required.
      */
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
     public void setDeviceToDeviceStatusSharingContacts(int subscriptionId,
@@ -3790,38 +3816,46 @@
     }
 
     /**
-     * Returns the list of contacts that allow device to device status sharing.
-     * @param subscriptionId Subscription id of subscription
-     * @return The list of contacts that allow device to device status sharing
+     * Get the list of contacts that allow device to device status sharing.
+     *
+     * @param subscriptionId Subscription id.
+     *
+     * @return The list of contacts that allow device to device status sharing.
      */
-    public @NonNull List<Uri> getDeviceToDeviceStatusSharingContacts(
-            int subscriptionId) {
-        if (VDBG) {
-            logd("[getDeviceToDeviceStatusSharingContacts] + subId: " + subscriptionId);
+    public @NonNull List<Uri> getDeviceToDeviceStatusSharingContacts(int subscriptionId) {
+        String result = getStringSubscriptionProperty(mContext, subscriptionId,
+                D2D_STATUS_SHARING_SELECTED_CONTACTS);
+        if (result != null) {
+            try {
+                byte[] b = Base64.decode(result, Base64.DEFAULT);
+                ByteArrayInputStream bis = new ByteArrayInputStream(b);
+                ObjectInputStream ois = new ObjectInputStream(bis);
+                List<String> contacts = ArrayList.class.cast(ois.readObject());
+                List<Uri> uris = new ArrayList<>();
+                for (String contact : contacts) {
+                    uris.add(Uri.parse(contact));
+                }
+                return uris;
+            } catch (IOException e) {
+                logd("getDeviceToDeviceStatusSharingContacts IO exception");
+            } catch (ClassNotFoundException e) {
+                logd("getDeviceToDeviceStatusSharingContacts ClassNotFound exception");
+            }
         }
-        return getContactsFromSubscriptionProperty(subscriptionId,
-                D2D_STATUS_SHARING_SELECTED_CONTACTS, mContext);
+        return new ArrayList<>();
     }
 
     /**
-     * Get the active subscription id by logical SIM slot index.
-     *
-     * @param slotIndex The logical SIM slot index.
-     * @return The active subscription id.
-     *
-     * @throws IllegalArgumentException if the provided slot index is invalid.
-     *
+     * DO NOT USE.
+     * This API is designed for features that are not finished at this point. Do not call this API.
      * @hide
+     * TODO b/135547512: further clean up
      */
     @SystemApi
     @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
     public int getEnabledSubscriptionId(int slotIndex) {
         int subId = INVALID_SUBSCRIPTION_ID;
 
-        if (!isValidSlotIndex(slotIndex)) {
-            throw new IllegalArgumentException("Invalid slot index " + slotIndex);
-        }
-
         try {
             ISub iSub = TelephonyManager.getSubscriptionService();
             if (iSub != null) {
@@ -3863,7 +3897,7 @@
     /**
      * Get active data subscription id. Active data subscription refers to the subscription
      * currently chosen to provide cellular internet connection to the user. This may be
-     * different from getDefaultDataSubscriptionId().
+     * different from {@link #getDefaultDataSubscriptionId()}.
      *
      * @return Active data subscription id if any is chosen, or {@link #INVALID_SUBSCRIPTION_ID} if
      * not.
@@ -4061,12 +4095,15 @@
      * security-related or other sensitive scenarios.
      *
      * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID}
-     *                       for the default one.
+     * for the default one.
      * @param source the source of the phone number, one of the PHONE_NUMBER_SOURCE_* constants.
+     *
      * @return the phone number, or an empty string if not available.
+     *
      * @throws IllegalArgumentException if {@code source} is invalid.
      * @throws IllegalStateException if the telephony process is not currently available.
      * @throws SecurityException if the caller doesn't have permissions required.
+     *
      * @see #PHONE_NUMBER_SOURCE_UICC
      * @see #PHONE_NUMBER_SOURCE_CARRIER
      * @see #PHONE_NUMBER_SOURCE_IMS
@@ -4123,8 +4160,10 @@
      * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID}
      *                       for the default one.
      * @return the phone number, or an empty string if not available.
+     *
      * @throws IllegalStateException if the telephony process is not currently available.
      * @throws SecurityException if the caller doesn't have permissions required.
+     *
      * @see #getPhoneNumber(int, int)
      */
     @RequiresPermission(anyOf = {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 09daa2d..5d49413 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -2935,7 +2935,7 @@
     public static final int NETWORK_TYPE_HSPA = TelephonyProtoEnums.NETWORK_TYPE_HSPA; // = 10.
     /**
      * Current network is iDen
-     * @deprecated Legacy network type no longer being used.
+     * @deprecated Legacy network type no longer being used starting in Android U.
      */
     @Deprecated
     public static final int NETWORK_TYPE_IDEN = TelephonyProtoEnums.NETWORK_TYPE_IDEN; // = 11.
@@ -13960,7 +13960,7 @@
      * If used, will be converted to {@link #NETWORK_TYPE_BITMASK_LTE}.
      * network type bitmask indicating the support of radio tech LTE CA (carrier aggregation).
      *
-     * @deprecated Please use {@link #NETWORK_TYPE_BITMASK_LTE} instead.
+     * @deprecated Please use {@link #NETWORK_TYPE_BITMASK_LTE} instead. Deprecated in Android U.
      */
     @Deprecated
     public static final long NETWORK_TYPE_BITMASK_LTE_CA = (1 << (NETWORK_TYPE_LTE_CA -1));
@@ -17808,11 +17808,11 @@
      * @hide
      */
     @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
-    public void isNullCipherAndIntegrityPreferenceEnabled() {
+    public boolean isNullCipherAndIntegrityPreferenceEnabled() {
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                telephony.isNullCipherAndIntegrityPreferenceEnabled();
+                return telephony.isNullCipherAndIntegrityPreferenceEnabled();
             } else {
                 throw new IllegalStateException("telephony service is null.");
             }
@@ -17820,5 +17820,6 @@
             Rlog.e(TAG, "isNullCipherAndIntegrityPreferenceEnabled RemoteException", ex);
             ex.rethrowFromSystemServer();
         }
+        return true;
     }
 }
diff --git a/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl b/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl
index 5246470..c8c8724 100644
--- a/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl
+++ b/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl
@@ -34,4 +34,6 @@
     void onIncomingCall(IImsCallSession c, in Bundle extras);
     void onRejectedCall(in ImsCallProfile callProfile, in ImsReasonInfo reason);
     oneway void onVoiceMessageCountUpdate(int count);
+    oneway void onAudioModeIsVoipChanged(int imsAudioHandler);
+    oneway void onTriggerEpsFallback(int reason);
 }
diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
index a380241..1c7e9b9 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -579,6 +579,28 @@
         public void onVoiceMessageCountUpdate(int count) {
 
         }
+
+        /**
+         * Called to set the audio handler for this connection.
+         * @param imsAudioHandler an {@link ImsAudioHandler} used to handle the audio
+         *        for this IMS call.
+         * @hide
+         */
+        @Override
+        public void onAudioModeIsVoipChanged(int imsAudioHandler) {
+
+        }
+
+        /**
+         * Called when the IMS triggers EPS fallback procedure.
+         *
+         * @param reason specifies the reason that causes EPS fallback.
+         * @hide
+         */
+        @Override
+        public void onTriggerEpsFallback(@EpsFallbackReason int reason) {
+
+        }
     }
 
     /**
@@ -628,6 +650,71 @@
     public static final String EXTRA_IS_UNKNOWN_CALL =
             "android.telephony.ims.feature.extra.IS_UNKNOWN_CALL";
 
+    /** @hide */
+    @IntDef(flag = true,
+            value = {
+                    AUDIO_HANDLER_ANDROID,
+                    AUDIO_HANDLER_BASEBAND
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ImsAudioHandler {}
+
+    /**
+    * Audio Handler - Android
+    * @hide
+    */
+    @SystemApi
+    public static final int AUDIO_HANDLER_ANDROID = 0;
+
+    /**
+    * Audio Handler - Baseband
+    * @hide
+    */
+    @SystemApi
+    public static final int AUDIO_HANDLER_BASEBAND = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+        prefix = "EPS_FALLBACK_REASON_",
+        value = {
+            EPS_FALLBACK_REASON_INVALID,
+            EPS_FALLBACK_REASON_NO_NETWORK_TRIGGER,
+            EPS_FALLBACK_REASON_NO_NETWORK_RESPONSE,
+        })
+    public @interface EpsFallbackReason {}
+
+    /**
+     * Default value. Internal use only.
+     * This value should not be used to trigger EPS fallback.
+     * @hide
+     */
+    public static final int EPS_FALLBACK_REASON_INVALID = -1;
+
+    /**
+     * If the network only supports the EPS fallback in 5G NR SA for voice calling and the EPS
+     * Fallback procedure by the network during the call setup is not triggered, UE initiated
+     * fallback will be triggered with this reason. The modem shall locally release the 5G NR
+     * SA RRC connection and acquire the LTE network and perform a tracking area update
+     * procedure. After the EPS fallback procedure is completed, the call setup for voice will
+     * be established if there is no problem.
+     *
+     * @hide
+     */
+    public static final int EPS_FALLBACK_REASON_NO_NETWORK_TRIGGER = 1;
+
+    /**
+     * If the UE doesn't receive any response for SIP INVITE within a certain timeout in 5G NR
+     * SA for MO voice calling, the device determines that voice call is not available in 5G and
+     * terminates all active SIP dialogs and SIP requests and enters IMS non-registered state.
+     * In that case, UE initiated fallback will be triggered with this reason. The modem shall
+     * reset modem's data buffer of IMS PDU to prevent the ghost call. After the EPS fallback
+     * procedure is completed, VoLTE call could be tried if there is no problem.
+     *
+     * @hide
+     */
+    public static final int EPS_FALLBACK_REASON_NO_NETWORK_RESPONSE = 2;
+
     private IImsMmTelListener mListener;
 
     /**
@@ -774,6 +861,46 @@
     }
 
     /**
+     * Sets the audio handler for this connection. The vendor IMS stack will invoke this API
+     * to inform Telephony/Telecom layers about which audio handlers i.e. either Android or Modem
+     * shall be used for handling the IMS call audio.
+     *
+     * @param imsAudioHandler {@link MmTelFeature#ImsAudioHandler} used to handle the audio
+     *        for this IMS call.
+     * @hide
+     */
+    @SystemApi
+    public final void setCallAudioHandler(@ImsAudioHandler int imsAudioHandler) {
+        IImsMmTelListener listener = getListener();
+        if (listener == null) {
+            throw new IllegalStateException("Session is not available.");
+        }
+        try {
+            listener.onAudioModeIsVoipChanged(imsAudioHandler);
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Triggers the EPS fallback procedure.
+     *
+     * @param reason specifies the reason that causes EPS fallback.
+     * @hide
+     */
+    public final void triggerEpsFallback(@EpsFallbackReason int reason) {
+        IImsMmTelListener listener = getListener();
+        if (listener == null) {
+            throw new IllegalStateException("Session is not available.");
+        }
+        try {
+            listener.onTriggerEpsFallback(reason);
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
      * Provides the MmTelFeature with the ability to return the framework Capability Configuration
      * for a provided Capability. If the framework calls {@link #changeEnabledCapabilities} and
      * includes a capability A to enable or disable, this method should return the correct enabled
diff --git a/telephony/java/android/telephony/ims/stub/ImsConfigImplBase.java b/telephony/java/android/telephony/ims/stub/ImsConfigImplBase.java
index 897b57f..ad8a936 100644
--- a/telephony/java/android/telephony/ims/stub/ImsConfigImplBase.java
+++ b/telephony/java/android/telephony/ims/stub/ImsConfigImplBase.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
+import android.telephony.ims.ImsService;
 import android.telephony.ims.ProvisioningManager;
 import android.telephony.ims.RcsClientConfiguration;
 import android.telephony.ims.RcsConfig;
@@ -553,7 +554,7 @@
     ImsConfigStub mImsConfigStub;
 
     /**
-     * Create a ImsConfig using the Executor specified for methods being called by the
+     * Create an ImsConfig using the Executor specified for methods being called by the
      * framework.
      * @param executor The executor for the framework to use when executing the methods overridden
      * by the implementation of ImsConfig.
@@ -569,6 +570,9 @@
         mImsConfigStub = new ImsConfigStub(this, null);
     }
 
+    /**
+     * Create an ImsConfig using the Executor defined in {@link ImsService#getExecutor}
+     */
     public ImsConfigImplBase() {
         mImsConfigStub = new ImsConfigStub(this, null);
     }
diff --git a/telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl b/telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl
index 89620ea..1788bda 100644
--- a/telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl
+++ b/telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl
@@ -38,4 +38,6 @@
 
     String getDefaultCarrierServicePackageName();
 
+    PersistableBundle getConfigSubsetForSubIdWithFeature(int subId, String callingPackage,
+                String callingFeatureId, in String[] carrierConfigs);
 }
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 280d259..c5f6902 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -242,8 +242,6 @@
 
     int getDefaultSubId();
 
-    int clearSubInfo();
-
     int getPhoneId(int subId);
 
     /**
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 a4609f7..948288a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -18,12 +18,14 @@
 
 import android.app.Instrumentation
 import android.platform.test.annotations.Presubmit
+import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
 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
+import org.junit.AssumptionViolatedException
 import org.junit.Test
 
 /**
@@ -48,6 +50,8 @@
         tapl.setExpectedRotationCheckEnabled(true)
     }
 
+    private val logTag = this::class.java.simpleName
+
     /** Specification of the test transition to execute */
     abstract val transition: FlickerBuilder.() -> Unit
 
@@ -100,10 +104,24 @@
     @Test
     open fun navBarWindowIsAlwaysVisible() {
         Assume.assumeFalse(flicker.scenario.isTablet)
+        Assume.assumeFalse(flicker.scenario.isLandscapeOrSeascapeAtStart)
         flicker.navBarWindowIsAlwaysVisible()
     }
 
     /**
+     * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible at the start and end of
+     * the transition
+     *
+     * Note: Phones only
+     */
+    @Presubmit
+    @Test
+    open fun navBarWindowIsVisibleAtStartAndEnd() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsVisibleAtStartAndEnd()
+    }
+
+    /**
      * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible at the start and end of the
      * transition
      *
@@ -179,13 +197,18 @@
         statusBarWindowIsAlwaysVisible()
         visibleLayersShownMoreThanOneConsecutiveEntry()
         visibleWindowsShownMoreThanOneConsecutiveEntry()
+        runAndIgnoreAssumptionViolation { taskBarLayerIsVisibleAtStartAndEnd() }
+        runAndIgnoreAssumptionViolation { taskBarWindowIsAlwaysVisible() }
+        runAndIgnoreAssumptionViolation { navBarLayerIsVisibleAtStartAndEnd() }
+        runAndIgnoreAssumptionViolation { navBarWindowIsAlwaysVisible() }
+        runAndIgnoreAssumptionViolation { navBarWindowIsVisibleAtStartAndEnd() }
+    }
 
-        if (flicker.scenario.isTablet) {
-            taskBarLayerIsVisibleAtStartAndEnd()
-            taskBarWindowIsAlwaysVisible()
-        } else {
-            navBarLayerIsVisibleAtStartAndEnd()
-            navBarWindowIsAlwaysVisible()
+    protected fun runAndIgnoreAssumptionViolation(predicate: () -> Unit) {
+        try {
+            predicate()
+        } catch (e: AssumptionViolatedException) {
+            Log.e(logTag, "Assumption violation on CUJ complete", e)
         }
     }
 }
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 afc5f65..098c082 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
@@ -106,10 +106,10 @@
     @IwTest(focusArea = "ime")
     override fun cujCompleted() {
         super.cujCompleted()
-        navBarLayerPositionAtStartAndEnd()
         imeLayerBecomesInvisible()
         imeAppLayerIsAlwaysVisible()
         imeAppWindowIsAlwaysVisible()
+        runAndIgnoreAssumptionViolation { navBarLayerPositionAtStartAndEnd() }
     }
 
     companion object {
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 aedf965..f110e54 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
@@ -104,11 +104,11 @@
     @IwTest(focusArea = "ime")
     override fun cujCompleted() {
         super.cujCompleted()
-        navBarLayerPositionAtStartAndEnd()
         imeLayerBecomesInvisible()
         imeAppWindowBecomesInvisible()
         imeWindowBecomesInvisible()
         imeLayerBecomesInvisible()
+        runAndIgnoreAssumptionViolation { navBarLayerPositionAtStartAndEnd() }
     }
 
     companion object {
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 c599b10..e297e89 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
@@ -21,8 +21,6 @@
 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
@@ -68,15 +66,4 @@
     @Ignore("Nav bar window becomes invisible during quick switch")
     @Test
     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
-     */
-    @Presubmit
-    @Test
-    fun navBarWindowIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.navBarWindowIsVisibleAtStartAndEnd()
-    }
 }
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 f5f7190..26898f8 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
@@ -89,6 +89,11 @@
     override fun statusBarLayerIsVisibleAtStartAndEnd() =
         super.statusBarLayerIsVisibleAtStartAndEnd()
 
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
     /**
      * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
      * transition
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 fe49c61..c44ad83 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
@@ -112,6 +112,11 @@
     /** {@inheritDoc} */
     @Test
     @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
     override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
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 d9a3ad2..e3ffb45 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
@@ -109,6 +109,11 @@
     @Test
     override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 209599395)
+    @Test
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
     companion object {
         /**
          * Creates the test configurations.
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 f295ce3..142c688 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
@@ -146,6 +146,11 @@
 
     /** {@inheritDoc} */
     @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
     @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
     override fun statusBarWindowIsAlwaysVisible() {}
 
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 4e7ab7a..b064695 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
@@ -20,6 +20,7 @@
 import android.app.WallpaperManager
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.FlickerBuilder
@@ -29,11 +30,15 @@
 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.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.DEFAULT_TASK_DISPLAY_AREA
 import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.SPLASH_SCREEN
 import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER
+import com.android.server.wm.traces.common.ComponentSplashScreenMatcher
 import com.android.server.wm.traces.common.IComponentMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
+import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -43,7 +48,7 @@
 /**
  * Test the back and forward transition between 2 activities.
  *
- * To run this test: `atest FlickerTests:ActivitiesTransitionTest`
+ * To run this test: `atest FlickerTests:TaskTransitionTest`
  *
  * Actions:
  * ```
@@ -57,7 +62,7 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) {
-    private val testApp = NewTasksAppHelper(instrumentation)
+    private val launchNewTaskApp = NewTasksAppHelper(instrumentation)
     private val simpleApp = SimpleAppHelper(instrumentation)
     private val wallpaper by lazy {
         getWallpaperPackage(instrumentation) ?: error("Unable to obtain wallpaper")
@@ -65,10 +70,10 @@
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
-        setup { testApp.launchViaIntent(wmHelper) }
-        teardown { testApp.exit(wmHelper) }
+        setup { launchNewTaskApp.launchViaIntent(wmHelper) }
+        teardown { launchNewTaskApp.exit(wmHelper) }
         transitions {
-            testApp.openNewTask(device, wmHelper)
+            launchNewTaskApp.openNewTask(device, wmHelper)
             tapl.pressBack()
             wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
         }
@@ -101,7 +106,7 @@
      * Check that the [ComponentNameMatcher.LAUNCHER] window is never visible when performing task
      * transitions. A solid color background should be shown above it.
      */
-    @Postsubmit
+    @Presubmit
     @Test
     fun launcherWindowIsNeverVisible() {
         flicker.assertWm { this.isAppWindowInvisible(ComponentNameMatcher.LAUNCHER) }
@@ -111,42 +116,76 @@
      * Checks that the [ComponentNameMatcher.LAUNCHER] layer is never visible when performing task
      * transitions. A solid color background should be shown above it.
      */
-    @Postsubmit
+    @Presubmit
     @Test
     fun launcherLayerIsNeverVisible() {
         flicker.assertLayers { this.isInvisible(ComponentNameMatcher.LAUNCHER) }
     }
 
     /** Checks that a color background is visible while the task transition is occurring. */
-    @FlakyTest(bugId = 240570652)
+    @Presubmit
     @Test
-    fun colorLayerIsVisibleDuringTransition() {
-        val bgColorLayer = ComponentNameMatcher("", "colorBackgroundLayer")
-        val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
+    fun transitionHasColorBackground_legacy() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
+        transitionHasColorBackground(DEFAULT_TASK_DISPLAY_AREA)
+    }
 
+    /** Checks that a color background is visible while the task transition is occurring. */
+    @Presubmit
+    @Test
+    fun transitionHasColorBackground_shellTransit() {
+        Assume.assumeTrue(isShellTransitionsEnabled)
+        transitionHasColorBackground(ComponentNameMatcher("", "Animation Background"))
+    }
+
+    private fun transitionHasColorBackground(backgroundColorLayer: IComponentMatcher) {
+        Assume.assumeTrue(isShellTransitionsEnabled)
+
+        val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
         flicker.assertLayers {
-            this.invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
-                    it.visibleRegion(testApp.componentMatcher).coversExactly(displayBounds)
+            this
+                .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
+                    it.visibleRegion(launchNewTaskApp.componentMatcher).coversExactly(displayBounds)
                 }
-                .isInvisible(bgColorLayer)
+                .isInvisible(backgroundColorLayer)
+                .hasNoColor(backgroundColorLayer)
                 .then()
                 // Transitioning
-                .isVisible(bgColorLayer)
+                .isVisible(backgroundColorLayer)
+                .hasColor(backgroundColorLayer)
                 .then()
                 // Fully transitioned to simple SIMPLE_ACTIVITY
+                .invoke(
+                    "SIMPLE_ACTIVITY's splashscreen coversExactly displayBounds",
+                    isOptional = true
+                ) {
+                    it.visibleRegion(ComponentSplashScreenMatcher( simpleApp.componentMatcher))
+                        .coversExactly(displayBounds)
+                }
                 .invoke("SIMPLE_ACTIVITY coversExactly displayBounds") {
                     it.visibleRegion(simpleApp.componentMatcher).coversExactly(displayBounds)
                 }
-                .isInvisible(bgColorLayer)
+                .isInvisible(backgroundColorLayer)
+                .hasNoColor(backgroundColorLayer)
                 .then()
                 // Transitioning back
-                .isVisible(bgColorLayer)
+                .isVisible(backgroundColorLayer)
+                .hasColor(backgroundColorLayer)
                 .then()
                 // Fully transitioned back to LAUNCH_NEW_TASK_ACTIVITY
-                .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
-                    it.visibleRegion(testApp.componentMatcher).coversExactly(displayBounds)
+                .invoke(
+                    "LAUNCH_NEW_TASK_ACTIVITY's splashscreen coversExactly displayBounds",
+                    isOptional = true
+                ) {
+                    it.visibleRegion(
+                            ComponentSplashScreenMatcher(launchNewTaskApp.componentMatcher))
+                        .coversExactly(displayBounds)
                 }
-                .isInvisible(bgColorLayer)
+                .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
+                    it.visibleRegion(launchNewTaskApp.componentMatcher).coversExactly(displayBounds)
+                }
+                .isInvisible(backgroundColorLayer)
+                .hasNoColor(backgroundColorLayer)
         }
     }
 
@@ -158,7 +197,7 @@
     @Test
     fun newTaskOpensOnTopAndThenCloses() {
         flicker.assertWm {
-            this.isAppWindowOnTop(testApp.componentMatcher)
+            this.isAppWindowOnTop(launchNewTaskApp.componentMatcher)
                 .then()
                 .isAppWindowOnTop(SPLASH_SCREEN, isOptional = true)
                 .then()
@@ -166,66 +205,15 @@
                 .then()
                 .isAppWindowOnTop(SPLASH_SCREEN, isOptional = true)
                 .then()
-                .isAppWindowOnTop(testApp.componentMatcher)
+                .isAppWindowOnTop(launchNewTaskApp.componentMatcher)
         }
     }
 
     /** {@inheritDoc} */
-    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
-    /** {@inheritDoc} */
     @Postsubmit
     @Test
     override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
     companion object {
         private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher? {
             val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext)
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 ec4e35c..6dc11b5 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
@@ -17,13 +17,10 @@
 package com.android.server.wm.flicker.quickswitch
 
 import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
 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
@@ -61,17 +58,6 @@
     @Test
     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
-     */
-    @Presubmit
-    @Test
-    fun navBarWindowIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.navBarWindowIsVisibleAtStartAndEnd()
-    }
-
     /** {@inheritDoc} */
     @FlakyTest(bugId = 250520840)
     @Test
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 477b419..5a78868 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
@@ -17,13 +17,10 @@
 package com.android.server.wm.flicker.quickswitch
 
 import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
 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
@@ -62,17 +59,6 @@
     @Test
     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
-     */
-    @Presubmit
-    @Test
-    fun navBarWindowIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.navBarWindowIsVisibleAtStartAndEnd()
-    }
-
     @FlakyTest(bugId = 246284708)
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
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 8c8220f..456eab1 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
@@ -26,7 +26,6 @@
 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
@@ -262,17 +261,6 @@
     @Test
     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
-     */
-    @Presubmit
-    @Test
-    fun navBarWindowIsVisibleAtStartAndEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.navBarWindowIsVisibleAtStartAndEnd()
-    }
-
     @Presubmit
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
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 5b52c75..e3296a5 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
@@ -127,7 +127,7 @@
     override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     @Test
-    @IwTest(focusArea = "ime")
+    @IwTest(focusArea = "framework")
     override fun cujCompleted() {
         super.cujCompleted()
         focusChanges()
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 54f38c3..741ae51 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
@@ -207,14 +207,8 @@
     override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     @Test
-    @IwTest(focusArea = "ime")
+    @IwTest(focusArea = "framework")
     override fun cujCompleted() {
-        if (!flicker.scenario.isTablet) {
-            // not yet tablet compatible
-            appLayerRotates()
-            appLayerAlwaysVisible()
-        }
-
         appWindowFullScreen()
         appWindowSeamlessRotation()
         focusDoesNotChange()
@@ -223,12 +217,15 @@
         appLayerRotates_StartingPos()
         appLayerRotates_EndingPos()
         entireScreenCovered()
-        navBarLayerIsVisibleAtStartAndEnd()
-        navBarWindowIsAlwaysVisible()
-        taskBarLayerIsVisibleAtStartAndEnd()
-        taskBarWindowIsAlwaysVisible()
         visibleLayersShownMoreThanOneConsecutiveEntry()
         visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+        runAndIgnoreAssumptionViolation { appLayerRotates() }
+        runAndIgnoreAssumptionViolation { appLayerAlwaysVisible() }
+        runAndIgnoreAssumptionViolation { navBarLayerIsVisibleAtStartAndEnd() }
+        runAndIgnoreAssumptionViolation { navBarWindowIsAlwaysVisible() }
+        runAndIgnoreAssumptionViolation { taskBarLayerIsVisibleAtStartAndEnd() }
+        runAndIgnoreAssumptionViolation { taskBarWindowIsAlwaysVisible() }
     }
 
     companion object {
diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
index 7ac51b7..b313c9f 100644
--- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
@@ -16,7 +16,12 @@
 
 package android.net.vcn;
 
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -24,6 +29,7 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.os.Parcel;
+import android.util.ArraySet;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -42,19 +48,36 @@
     private static final Set<VcnGatewayConnectionConfig> GATEWAY_CONNECTION_CONFIGS =
             Collections.singleton(VcnGatewayConnectionConfigTest.buildTestConfig());
 
+    private static final Set<Integer> RESTRICTED_TRANSPORTS = new ArraySet<>();
+
+    static {
+        RESTRICTED_TRANSPORTS.add(TRANSPORT_WIFI);
+        RESTRICTED_TRANSPORTS.add(TRANSPORT_CELLULAR);
+    }
+
     private final Context mContext = mock(Context.class);
 
     // Public visibility for VcnManagementServiceTest
-    public static VcnConfig buildTestConfig(@NonNull Context context) {
+    public static VcnConfig buildTestConfig(
+            @NonNull Context context, Set<Integer> restrictedTransports) {
         VcnConfig.Builder builder = new VcnConfig.Builder(context);
 
         for (VcnGatewayConnectionConfig gatewayConnectionConfig : GATEWAY_CONNECTION_CONFIGS) {
             builder.addGatewayConnectionConfig(gatewayConnectionConfig);
         }
 
+        if (restrictedTransports != null) {
+            builder.setRestrictedUnderlyingNetworkTransports(restrictedTransports);
+        }
+
         return builder.build();
     }
 
+    // Public visibility for VcnManagementServiceTest
+    public static VcnConfig buildTestConfig(@NonNull Context context) {
+        return buildTestConfig(context, null);
+    }
+
     @Before
     public void setUp() throws Exception {
         doReturn(TEST_PACKAGE_NAME).when(mContext).getOpPackageName();
@@ -91,11 +114,25 @@
     }
 
     @Test
-    public void testBuilderAndGetters() {
+    public void testBuilderAndGettersDefaultValues() {
         final VcnConfig config = buildTestConfig(mContext);
 
         assertEquals(TEST_PACKAGE_NAME, config.getProvisioningPackageName());
         assertEquals(GATEWAY_CONNECTION_CONFIGS, config.getGatewayConnectionConfigs());
+        assertFalse(config.isTestModeProfile());
+        assertEquals(
+                Collections.singleton(TRANSPORT_WIFI),
+                config.getRestrictedUnderlyingNetworkTransports());
+    }
+
+    @Test
+    public void testBuilderAndGettersConfigRestrictedTransports() {
+        final VcnConfig config = buildTestConfig(mContext, RESTRICTED_TRANSPORTS);
+
+        assertEquals(TEST_PACKAGE_NAME, config.getProvisioningPackageName());
+        assertEquals(GATEWAY_CONNECTION_CONFIGS, config.getGatewayConnectionConfigs());
+        assertFalse(config.isTestModeProfile());
+        assertEquals(RESTRICTED_TRANSPORTS, config.getRestrictedUnderlyingNetworkTransports());
     }
 
     @Test
@@ -106,6 +143,24 @@
     }
 
     @Test
+    public void testPersistableBundleWithRestrictedTransports() {
+        final VcnConfig config = buildTestConfig(mContext, RESTRICTED_TRANSPORTS);
+
+        assertEquals(config, new VcnConfig(config.toPersistableBundle()));
+    }
+
+    @Test
+    public void testEqualityWithRestrictedTransports() {
+        final VcnConfig config = buildTestConfig(mContext, RESTRICTED_TRANSPORTS);
+        final VcnConfig configEqual = buildTestConfig(mContext, RESTRICTED_TRANSPORTS);
+        final VcnConfig configNotEqual =
+                buildTestConfig(mContext, Collections.singleton(TRANSPORT_WIFI));
+
+        assertEquals(config, configEqual);
+        assertNotEquals(config, configNotEqual);
+    }
+
+    @Test
     public void testParceling() {
         final VcnConfig config = buildTestConfig(mContext);
 
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
index 2aef9ae..4040888 100644
--- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -19,9 +19,11 @@
 import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_MOBIKE;
 import static android.net.vcn.VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES;
 import static android.net.vcn.VcnGatewayConnectionConfig.UNDERLYING_NETWORK_TEMPLATES_KEY;
+import static android.net.vcn.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
@@ -42,7 +44,9 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
@@ -79,6 +83,9 @@
             };
     public static final int MAX_MTU = 1360;
 
+    private static final Set<Integer> GATEWAY_OPTIONS =
+            Collections.singleton(VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY);
+
     public static final IkeTunnelConnectionParams TUNNEL_CONNECTION_PARAMS =
             TunnelConnectionParamsUtilsTest.buildTestParams();
 
@@ -109,10 +116,16 @@
                 TUNNEL_CONNECTION_PARAMS);
     }
 
-    private static VcnGatewayConnectionConfig buildTestConfigWithExposedCaps(
-            VcnGatewayConnectionConfig.Builder builder, int... exposedCaps) {
+    private static VcnGatewayConnectionConfig buildTestConfigWithExposedCapsAndOptions(
+            VcnGatewayConnectionConfig.Builder builder,
+            Set<Integer> gatewayOptions,
+            int... exposedCaps) {
         builder.setRetryIntervalsMillis(RETRY_INTERVALS_MS).setMaxMtu(MAX_MTU);
 
+        for (int option : gatewayOptions) {
+            builder.addGatewayOption(option);
+        }
+
         for (int caps : exposedCaps) {
             builder.addExposedCapability(caps);
         }
@@ -120,11 +133,28 @@
         return builder.build();
     }
 
+    private static VcnGatewayConnectionConfig buildTestConfigWithExposedCaps(
+            VcnGatewayConnectionConfig.Builder builder, int... exposedCaps) {
+        return buildTestConfigWithExposedCapsAndOptions(
+                builder, Collections.emptySet(), exposedCaps);
+    }
+
     // Public for use in VcnGatewayConnectionTest
     public static VcnGatewayConnectionConfig buildTestConfigWithExposedCaps(int... exposedCaps) {
         return buildTestConfigWithExposedCaps(newBuilder(), exposedCaps);
     }
 
+    private static VcnGatewayConnectionConfig buildTestConfigWithGatewayOptions(
+            VcnGatewayConnectionConfig.Builder builder, Set<Integer> gatewayOptions) {
+        return buildTestConfigWithExposedCapsAndOptions(builder, gatewayOptions, EXPOSED_CAPS);
+    }
+
+    // Public for use in VcnGatewayConnectionTest
+    public static VcnGatewayConnectionConfig buildTestConfigWithGatewayOptions(
+            Set<Integer> gatewayOptions) {
+        return buildTestConfigWithExposedCapsAndOptions(newBuilder(), gatewayOptions, EXPOSED_CAPS);
+    }
+
     @Test
     public void testBuilderRequiresNonNullGatewayConnectionName() {
         try {
@@ -211,6 +241,15 @@
     }
 
     @Test
+    public void testBuilderRequiresValidOption() {
+        try {
+            newBuilder().addGatewayOption(-1);
+            fail("Expected exception due to the invalid VCN gateway option");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
     public void testBuilderAndGetters() {
         final VcnGatewayConnectionConfig config = buildTestConfig();
 
@@ -225,6 +264,20 @@
 
         assertArrayEquals(RETRY_INTERVALS_MS, config.getRetryIntervalsMillis());
         assertEquals(MAX_MTU, config.getMaxMtu());
+
+        assertFalse(
+                config.hasGatewayOption(
+                        VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY));
+    }
+
+    @Test
+    public void testBuilderAndGettersWithOptions() {
+        final VcnGatewayConnectionConfig config =
+                buildTestConfigWithGatewayOptions(GATEWAY_OPTIONS);
+
+        for (int option : GATEWAY_OPTIONS) {
+            assertTrue(config.hasGatewayOption(option));
+        }
     }
 
     @Test
@@ -235,6 +288,14 @@
     }
 
     @Test
+    public void testPersistableBundleWithOptions() {
+        final VcnGatewayConnectionConfig config =
+                buildTestConfigWithGatewayOptions(GATEWAY_OPTIONS);
+
+        assertEquals(config, new VcnGatewayConnectionConfig(config.toPersistableBundle()));
+    }
+
+    @Test
     public void testParsePersistableBundleWithoutVcnUnderlyingNetworkTemplates() {
         PersistableBundle configBundle = buildTestConfig().toPersistableBundle();
         configBundle.putPersistableBundle(UNDERLYING_NETWORK_TEMPLATES_KEY, null);
@@ -318,4 +379,27 @@
         assertNotEquals(UNDERLYING_NETWORK_TEMPLATES, networkTemplatesNotEqual);
         assertNotEquals(config, configNotEqual);
     }
+
+    private static VcnGatewayConnectionConfig buildConfigWithGatewayOptionsForEqualityTest(
+            Set<Integer> gatewayOptions) {
+        return buildTestConfigWithGatewayOptions(
+                new VcnGatewayConnectionConfig.Builder(
+                        "buildConfigWithGatewayOptionsForEqualityTest", TUNNEL_CONNECTION_PARAMS),
+                gatewayOptions);
+    }
+
+    @Test
+    public void testVcnGatewayOptionsEquality() throws Exception {
+        final VcnGatewayConnectionConfig config =
+                buildConfigWithGatewayOptionsForEqualityTest(GATEWAY_OPTIONS);
+
+        final VcnGatewayConnectionConfig configEqual =
+                buildConfigWithGatewayOptionsForEqualityTest(GATEWAY_OPTIONS);
+
+        final VcnGatewayConnectionConfig configNotEqual =
+                buildConfigWithGatewayOptionsForEqualityTest(Collections.emptySet());
+
+        assertEquals(config, configEqual);
+        assertNotEquals(config, configNotEqual);
+    }
 }
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 258642ac..075bc5e 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -258,7 +258,7 @@
 
         doReturn(Collections.singleton(TRANSPORT_WIFI))
                 .when(mMockDeps)
-                .getRestrictedTransports(any(), any());
+                .getRestrictedTransports(any(), any(), any());
     }
 
 
@@ -1038,18 +1038,18 @@
                 new LinkProperties());
     }
 
-    private void checkGetRestrictedTransports(
+    private void checkGetRestrictedTransportsFromCarrierConfig(
             ParcelUuid subGrp,
             TelephonySubscriptionSnapshot lastSnapshot,
             Set<Integer> expectedTransports) {
         Set<Integer> result =
                 new VcnManagementService.Dependencies()
-                        .getRestrictedTransports(subGrp, lastSnapshot);
+                        .getRestrictedTransportsFromCarrierConfig(subGrp, lastSnapshot);
         assertEquals(expectedTransports, result);
     }
 
     @Test
-    public void testGetRestrictedTransports() {
+    public void testGetRestrictedTransportsFromCarrierConfig() {
         final Set<Integer> restrictedTransports = new ArraySet<>();
         restrictedTransports.add(TRANSPORT_CELLULAR);
         restrictedTransports.add(TRANSPORT_WIFI);
@@ -1065,11 +1065,12 @@
                 mock(TelephonySubscriptionSnapshot.class);
         doReturn(carrierConfig).when(lastSnapshot).getCarrierConfigForSubGrp(eq(TEST_UUID_2));
 
-        checkGetRestrictedTransports(TEST_UUID_2, lastSnapshot, restrictedTransports);
+        checkGetRestrictedTransportsFromCarrierConfig(
+                TEST_UUID_2, lastSnapshot, restrictedTransports);
     }
 
     @Test
-    public void testGetRestrictedTransports_noRestrictPolicyConfigured() {
+    public void testGetRestrictedTransportsFromCarrierConfig_noRestrictPolicyConfigured() {
         final Set<Integer> restrictedTransports = Collections.singleton(TRANSPORT_WIFI);
 
         final PersistableBundleWrapper carrierConfig =
@@ -1078,17 +1079,54 @@
                 mock(TelephonySubscriptionSnapshot.class);
         doReturn(carrierConfig).when(lastSnapshot).getCarrierConfigForSubGrp(eq(TEST_UUID_2));
 
-        checkGetRestrictedTransports(TEST_UUID_2, lastSnapshot, restrictedTransports);
+        checkGetRestrictedTransportsFromCarrierConfig(
+                TEST_UUID_2, lastSnapshot, restrictedTransports);
     }
 
     @Test
-    public void testGetRestrictedTransports_noCarrierConfig() {
+    public void testGetRestrictedTransportsFromCarrierConfig_noCarrierConfig() {
         final Set<Integer> restrictedTransports = Collections.singleton(TRANSPORT_WIFI);
 
         final TelephonySubscriptionSnapshot lastSnapshot =
                 mock(TelephonySubscriptionSnapshot.class);
 
-        checkGetRestrictedTransports(TEST_UUID_2, lastSnapshot, restrictedTransports);
+        checkGetRestrictedTransportsFromCarrierConfig(
+                TEST_UUID_2, lastSnapshot, restrictedTransports);
+    }
+
+    @Test
+    public void testGetRestrictedTransportsFromCarrierConfigAndVcnConfig() {
+        // Configure restricted transport in CarrierConfig
+        final Set<Integer> restrictedTransportInCarrierConfig =
+                Collections.singleton(TRANSPORT_WIFI);
+
+        PersistableBundle carrierConfigBundle = new PersistableBundle();
+        carrierConfigBundle.putIntArray(
+                VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
+                restrictedTransportInCarrierConfig.stream().mapToInt(i -> i).toArray());
+        final PersistableBundleWrapper carrierConfig =
+                new PersistableBundleWrapper(carrierConfigBundle);
+
+        final TelephonySubscriptionSnapshot lastSnapshot =
+                mock(TelephonySubscriptionSnapshot.class);
+        doReturn(carrierConfig).when(lastSnapshot).getCarrierConfigForSubGrp(eq(TEST_UUID_2));
+
+        // Configure restricted transport in VcnConfig
+        final Context mockContext = mock(Context.class);
+        doReturn(TEST_PACKAGE_NAME).when(mockContext).getOpPackageName();
+        final VcnConfig vcnConfig =
+                VcnConfigTest.buildTestConfig(
+                        mockContext, Collections.singleton(TRANSPORT_CELLULAR));
+
+        // Verifications
+        final Set<Integer> expectedTransports = new ArraySet<>();
+        expectedTransports.add(TRANSPORT_CELLULAR);
+        expectedTransports.add(TRANSPORT_WIFI);
+
+        Set<Integer> result =
+                new VcnManagementService.Dependencies()
+                        .getRestrictedTransports(TEST_UUID_2, lastSnapshot, vcnConfig);
+        assertEquals(expectedTransports, result);
     }
 
     private void checkGetUnderlyingNetworkPolicy(
@@ -1103,7 +1141,7 @@
         if (isTransportRestricted) {
             restrictedTransports.add(transportType);
         }
-        doReturn(restrictedTransports).when(mMockDeps).getRestrictedTransports(any(), any());
+        doReturn(restrictedTransports).when(mMockDeps).getRestrictedTransports(any(), any(), any());
 
         final VcnUnderlyingNetworkPolicy policy =
                 startVcnAndGetPolicyForTransport(
@@ -1201,7 +1239,7 @@
     public void testGetUnderlyingNetworkPolicyCell_restrictWifi() throws Exception {
         doReturn(Collections.singleton(TRANSPORT_WIFI))
                 .when(mMockDeps)
-                .getRestrictedTransports(any(), any());
+                .getRestrictedTransports(any(), any(), any());
 
         setupSubscriptionAndStartVcn(TEST_SUBSCRIPTION_ID, TEST_UUID_2, true /* isVcnActive */);
 
@@ -1344,6 +1382,23 @@
     }
 
     @Test
+    public void testVcnConfigChangeUpdatesPolicyListener() throws Exception {
+        setupActiveSubscription(TEST_UUID_2);
+
+        mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
+        mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListener(mMockPolicyListener);
+
+        final Context mockContext = mock(Context.class);
+        doReturn(TEST_PACKAGE_NAME).when(mockContext).getOpPackageName();
+        final VcnConfig vcnConfig =
+                VcnConfigTest.buildTestConfig(
+                        mockContext, Collections.singleton(TRANSPORT_CELLULAR));
+        mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, vcnConfig, TEST_PACKAGE_NAME);
+
+        verify(mMockPolicyListener).onPolicyChanged();
+    }
+
+    @Test
     public void testRemoveVcnUpdatesPolicyListener() throws Exception {
         setupActiveSubscription(TEST_UUID_2);
 
@@ -1375,7 +1430,7 @@
         setupActiveSubscription(TEST_UUID_2);
 
         mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
-        mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListenerForTest(mMockPolicyListener);
+        mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListener(mMockPolicyListener);
 
         final TelephonySubscriptionSnapshot snapshot =
                 buildSubscriptionSnapshot(
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index 15d4f10..1c21a06 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -50,9 +50,11 @@
 
 import static java.util.Collections.singletonList;
 
+import android.net.ConnectivityDiagnosticsManager.DataStallReport;
 import android.net.ConnectivityManager;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
+import android.net.Network;
 import android.net.NetworkAgent;
 import android.net.NetworkCapabilities;
 import android.net.ipsec.ike.ChildSaProposal;
@@ -63,10 +65,12 @@
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnGatewayConnectionConfigTest;
 import android.net.vcn.VcnManager.VcnErrorCode;
+import android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
 import com.android.server.vcn.util.MtuUtils;
 
 import org.junit.Before;
@@ -88,6 +92,7 @@
 public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase {
     private VcnIkeSession mIkeSession;
     private VcnNetworkAgent mNetworkAgent;
+    private Network mVcnNetwork;
 
     @Before
     public void setUp() throws Exception {
@@ -98,6 +103,9 @@
                 .when(mDeps)
                 .newNetworkAgent(any(), any(), any(), any(), any(), any(), any(), any(), any());
 
+        mVcnNetwork = mock(Network.class);
+        doReturn(mVcnNetwork).when(mNetworkAgent).getNetwork();
+
         mGatewayConnection.setUnderlyingNetwork(TEST_UNDERLYING_NETWORK_RECORD_1);
 
         mIkeSession = mGatewayConnection.buildIkeSession(TEST_UNDERLYING_NETWORK_RECORD_1.network);
@@ -166,6 +174,56 @@
         assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
     }
 
+    private void verifyDataStallTriggersMigration(
+            UnderlyingNetworkRecord networkRecord,
+            Network networkWithDataStall,
+            boolean expectMobilityUpdate)
+            throws Exception {
+        mGatewayConnection.setUnderlyingNetwork(networkRecord);
+        triggerChildOpened();
+        mTestLooper.dispatchAll();
+
+        final DataStallReport report =
+                new DataStallReport(
+                        networkWithDataStall,
+                        1234 /* reportTimestamp */,
+                        1 /* detectionMethod */,
+                        new LinkProperties(),
+                        new NetworkCapabilities(),
+                        new PersistableBundle());
+
+        mGatewayConnection.getConnectivityDiagnosticsCallback().onDataStallSuspected(report);
+        mTestLooper.dispatchAll();
+
+        assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
+
+        if (expectMobilityUpdate) {
+            verify(mIkeSession).setNetwork(networkRecord.network);
+        } else {
+            verify(mIkeSession, never()).setNetwork(any(Network.class));
+        }
+    }
+
+    @Test
+    public void testDataStallTriggersMigration() throws Exception {
+        verifyDataStallTriggersMigration(
+                TEST_UNDERLYING_NETWORK_RECORD_1, mVcnNetwork, true /* expectMobilityUpdate */);
+    }
+
+    @Test
+    public void testDataStallWontTriggerMigrationWhenOnOtherNetwork() throws Exception {
+        verifyDataStallTriggersMigration(
+                TEST_UNDERLYING_NETWORK_RECORD_1,
+                mock(Network.class),
+                false /* expectMobilityUpdate */);
+    }
+
+    @Test
+    public void testDataStallWontTriggerMigrationWhenUnderlyingNetworkLost() throws Exception {
+        verifyDataStallTriggersMigration(
+                null /* networkRecord */, mock(Network.class), false /* expectMobilityUpdate */);
+    }
+
     private void verifyVcnTransformsApplied(
             VcnGatewayConnection vcnGatewayConnection, boolean expectForwardTransform)
             throws Exception {
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index 6a9a1e2..a4ee2de 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -24,6 +24,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.vcn.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY;
 
 import static com.android.server.vcn.VcnGatewayConnection.DUMMY_ADDR;
 import static com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration;
@@ -34,20 +35,25 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.CALLS_REAL_METHODS;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
 import android.net.IpSecManager;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnGatewayConnectionConfigTest;
 import android.net.vcn.VcnTransportInfo;
 import android.net.wifi.WifiInfo;
@@ -64,6 +70,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 
 import java.net.InetAddress;
 import java.util.Arrays;
@@ -71,7 +78,9 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.Executor;
 
 /** Tests for TelephonySubscriptionTracker */
 @RunWith(AndroidJUnit4.class)
@@ -287,5 +296,60 @@
         verify(vcnNetworkAgent).unregister();
 
         verifyWakeLockReleased();
+
+        verify(mConnDiagMgr)
+                .unregisterConnectivityDiagnosticsCallback(
+                        mGatewayConnection.getConnectivityDiagnosticsCallback());
+    }
+
+    private VcnGatewayConnection buildConnectionWithDataStallHandling(
+            boolean datatStallHandlingEnabled) throws Exception {
+        Set<Integer> options =
+                datatStallHandlingEnabled
+                        ? Collections.singleton(
+                                VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY)
+                        : Collections.emptySet();
+        final VcnGatewayConnectionConfig gatewayConfig =
+                VcnGatewayConnectionConfigTest.buildTestConfigWithGatewayOptions(options);
+        final VcnGatewayConnection gatewayConnection =
+                new VcnGatewayConnection(
+                        mVcnContext,
+                        TEST_SUB_GRP,
+                        TEST_SUBSCRIPTION_SNAPSHOT,
+                        gatewayConfig,
+                        mGatewayStatusCallback,
+                        true /* isMobileDataEnabled */,
+                        mDeps);
+        return gatewayConnection;
+    }
+
+    @Test
+    public void testDataStallHandlingEnabled() throws Exception {
+        final VcnGatewayConnection gatewayConnection =
+                buildConnectionWithDataStallHandling(true /* datatStallHandlingEnabled */);
+
+        final ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+                ArgumentCaptor.forClass(NetworkRequest.class);
+        verify(mConnDiagMgr)
+                .registerConnectivityDiagnosticsCallback(
+                        networkRequestCaptor.capture(),
+                        any(Executor.class),
+                        eq(gatewayConnection.getConnectivityDiagnosticsCallback()));
+
+        final NetworkRequest nr = networkRequestCaptor.getValue();
+        final NetworkRequest expected =
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build();
+        assertEquals(expected, nr);
+    }
+
+    @Test
+    public void testDataStallHandlingDisabled() throws Exception {
+        buildConnectionWithDataStallHandling(false /* datatStallHandlingEnabled */);
+
+        verify(mConnDiagMgr, never())
+                .registerConnectivityDiagnosticsCallback(
+                        any(NetworkRequest.class),
+                        any(Executor.class),
+                        any(ConnectivityDiagnosticsCallback.class));
     }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 785bff1..7bafd24 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -35,6 +35,7 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.net.ConnectivityDiagnosticsManager;
 import android.net.ConnectivityManager;
 import android.net.InetAddresses;
 import android.net.IpSecConfig;
@@ -157,6 +158,7 @@
 
     @NonNull protected final IpSecService mIpSecSvc;
     @NonNull protected final ConnectivityManager mConnMgr;
+    @NonNull protected final ConnectivityDiagnosticsManager mConnDiagMgr;
 
     @NonNull protected final IkeSessionConnectionInfo mIkeConnectionInfo;
     @NonNull protected final IkeSessionConfiguration mIkeSessionConfiguration;
@@ -186,6 +188,13 @@
         VcnTestUtils.setupSystemService(
                 mContext, mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
 
+        mConnDiagMgr = mock(ConnectivityDiagnosticsManager.class);
+        VcnTestUtils.setupSystemService(
+                mContext,
+                mConnDiagMgr,
+                Context.CONNECTIVITY_DIAGNOSTICS_SERVICE,
+                ConnectivityDiagnosticsManager.class);
+
         mIkeConnectionInfo =
                 new IkeSessionConnectionInfo(TEST_ADDR, TEST_ADDR_2, mock(Network.class));
         mIkeSessionConfiguration = new IkeSessionConfiguration.Builder(mIkeConnectionInfo).build();
diff --git a/tools/aapt2/cmd/Dump_test.cpp b/tools/aapt2/cmd/Dump_test.cpp
index b1c69cd..df35ebb 100644
--- a/tools/aapt2/cmd/Dump_test.cpp
+++ b/tools/aapt2/cmd/Dump_test.cpp
@@ -108,4 +108,21 @@
   ASSERT_EQ(output, expected);
 }
 
+TEST_F(DumpTest, DumpBadgingApkBuiltWithAaptAndTagsInWrongPlace) {
+  auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "integration-tests",
+                                   "DumpTest", "built_with_aapt.apk"});
+  auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+  std::string output;
+  DumpBadgingToString(loaded_apk.get(), &output, /* include_meta_data= */ false,
+                      /* only_permissions= */ false);
+
+  std::string expected;
+  auto expected_path =
+      file::BuildPath({android::base::GetExecutableDirectory(), "integration-tests", "DumpTest",
+                       "built_with_aapt_expected.txt"});
+  ::android::base::ReadFileToString(expected_path, &expected);
+  ASSERT_EQ(output, expected);
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/dump/DumpManifest.cpp b/tools/aapt2/dump/DumpManifest.cpp
index 76a6db6..d1957fb 100644
--- a/tools/aapt2/dump/DumpManifest.cpp
+++ b/tools/aapt2/dump/DumpManifest.cpp
@@ -223,7 +223,8 @@
     Element() = default;
     virtual ~Element() = default;
 
-    static std::unique_ptr<Element> Inflate(ManifestExtractor* extractor, xml::Element* el);
+    static std::unique_ptr<Element> Inflate(ManifestExtractor* extractor, xml::Element* el,
+                                            const std::string& parent_tag);
 
     /** Writes out the extracted contents of the element. */
     virtual void Print(text::Printer* printer) {
@@ -249,10 +250,15 @@
     }
 
     /** Retrieves the extracted xml element tag. */
-    const std::string tag() const {
+    const std::string& tag() const {
       return tag_;
     }
 
+    /** Whether this element has special Extract/Print/ToProto logic. */
+    bool is_featured() const {
+      return featured_;
+    }
+
    protected:
     ManifestExtractor* extractor() const {
       return extractor_;
@@ -394,6 +400,8 @@
               return &(*intValue->value);
             } else if (RawString* rawValue = ValueCast<RawString>(value)) {
               return &(*rawValue->value);
+            } else if (StyledString* styledStrValue = ValueCast<StyledString>(value)) {
+              return &(styledStrValue->value->value);
             } else if (FileReference* strValue = ValueCast<FileReference>(value)) {
               return &(*strValue->path);
             }
@@ -424,6 +432,7 @@
       ManifestExtractor* extractor_;
       std::vector<std::unique_ptr<Element>> children_;
       std::string tag_;
+      bool featured_ = false;
   };
 
   friend Element;
@@ -446,7 +455,7 @@
   bool DumpProto(pb::Badging* out_badging);
 
   /** Recursively visit the xml element tree and return a processed badging element tree. */
-  std::unique_ptr<Element> Visit(xml::Element* element);
+  std::unique_ptr<Element> Visit(xml::Element* element, const std::string& parent_tag);
 
   /** Resets target SDK to 0. */
   void ResetTargetSdk() {
@@ -485,7 +494,7 @@
   }
 
   /** Retrieves the current stack of parent during data extraction. */
-  const std::vector<Element*> parent_stack() const {
+  const std::vector<Element*>& parent_stack() const {
     return parent_stack_;
   }
 
@@ -533,8 +542,9 @@
   if (f(root)) {
     return root;
   }
-  for (auto& child : root->children()) {
-    if (auto b2 = FindElement(child.get(), f)) {
+  const auto& children = root->children();
+  for (auto it = children.rbegin(); it != children.rend(); ++it) {
+    if (auto b2 = FindElement(it->get(), f)) {
       return b2;
     }
   }
@@ -1348,11 +1358,6 @@
   std::string impliedReason;
 
   void Extract(xml::Element* element) override {
-    const auto parent_stack = extractor()->parent_stack();
-    if (!extractor()->options_.only_permissions &&
-        (parent_stack.size() != 1 || !ElementCast<Manifest>(parent_stack[0]))) {
-      return;
-    }
     name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
     std::string feature =
         GetAttributeStringDefault(FindAttribute(element, REQUIRED_FEATURE_ATTR), "");
@@ -1477,11 +1482,6 @@
   const int32_t* maxSdkVersion = nullptr;
 
   void Extract(xml::Element* element) override {
-    const auto parent_stack = extractor()->parent_stack();
-    if (!extractor()->options_.only_permissions &&
-        (parent_stack.size() != 1 || !ElementCast<Manifest>(parent_stack[0]))) {
-      return;
-    }
     name = GetAttributeString(FindAttribute(element, NAME_ATTR));
     maxSdkVersion = GetAttributeInteger(FindAttribute(element, MAX_SDK_VERSION_ATTR));
 
@@ -1717,11 +1717,8 @@
   int required;
 
   void Extract(xml::Element* element) override {
-    auto parent_stack = extractor()->parent_stack();
-    if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
-      name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
-      required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1);
-    }
+    name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+    required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1);
   }
 
   void Print(text::Printer* printer) override {
@@ -1749,12 +1746,9 @@
   int versionMajor;
 
   void Extract(xml::Element* element) override {
-    auto parent_stack = extractor()->parent_stack();
-    if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
-      name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
-      version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
-      versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
-    }
+    name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+    version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
+    versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
   }
 
   void Print(text::Printer* printer) override {
@@ -1781,13 +1775,10 @@
   std::vector<std::string> certDigests;
 
   void Extract(xml::Element* element) override {
-    auto parent_stack = extractor()->parent_stack();
-    if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
-      name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
-      version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
-      versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
-      AddCertDigest(element);
-    }
+    name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+    version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
+    versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
+    AddCertDigest(element);
   }
 
   void AddCertDigest(xml::Element* element) {
@@ -1829,11 +1820,8 @@
   int versionMajor;
 
   void Extract(xml::Element* element) override {
-    auto parent_stack = extractor()->parent_stack();
-    if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
-      name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
-      versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
-    }
+    name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+    versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
   }
 
   void Print(text::Printer* printer) override {
@@ -1857,12 +1845,9 @@
   std::vector<std::string> certDigests;
 
   void Extract(xml::Element* element) override {
-    auto parent_stack = extractor()->parent_stack();
-    if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
-      name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
-      versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
-      AddCertDigest(element);
-    }
+    name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+    versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
+    AddCertDigest(element);
   }
 
   void AddCertDigest(xml::Element* element) {
@@ -1902,11 +1887,8 @@
   int required;
 
   void Extract(xml::Element* element) override {
-    auto parent_stack = extractor()->parent_stack();
-    if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
-      name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
-      required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1);
-    }
+    name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+    required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1);
   }
 
   void Print(text::Printer* printer) override {
@@ -2251,14 +2233,11 @@
   std::vector<std::string> certDigests;
 
   void Extract(xml::Element* element) override {
-    auto parent_stack = extractor()->parent_stack();
-    if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
-      packageType = GetAttributeString(FindAttribute(element, PACKAGE_TYPE_ATTR));
-      name = GetAttributeString(FindAttribute(element, NAME_ATTR));
-      version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
-      versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
-      AddCertDigest(element);
-    }
+    packageType = GetAttributeString(FindAttribute(element, PACKAGE_TYPE_ATTR));
+    name = GetAttributeString(FindAttribute(element, NAME_ATTR));
+    version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
+    versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
+    AddCertDigest(element);
   }
 
   void AddCertDigest(xml::Element* element) {
@@ -2480,7 +2459,7 @@
   // Print only the <uses-permission>, <uses-permission-sdk23>, and <permission> elements if
   // printing only permission elements is requested
   if (options_.only_permissions) {
-    root_element_ = ManifestExtractor::Element::Inflate(this, element);
+    root_element_ = ManifestExtractor::Element::Inflate(this, element, "");
 
     if (auto manifest = ElementCast<Manifest>(root_element_.get())) {
       manifest->only_package_name = true;
@@ -2489,7 +2468,7 @@
         if (child->name == "uses-permission" || child->name == "uses-permission-sdk-23"
             || child->name == "permission") {
           // Inflate the element and its descendants
-          auto permission_element = Visit(child);
+          auto permission_element = Visit(child, "manifest");
           manifest->AddChild(permission_element);
         }
       }
@@ -2528,7 +2507,7 @@
   }
 
   // Extract badging information
-  root_element_ = Visit(element);
+  root_element_ = Visit(element, "");
 
   // Filter out all "uses-sdk" tags besides the very last tag. The android runtime only uses the
   // attribute values from the last defined tag.
@@ -2683,7 +2662,7 @@
                            (meta_data->name == "android.nfc.cardemulation.off_host_apdu_service" &&
                             offhost_apdu_action)) {
                          // Attempt to load the resource file
-                         if (!meta_data->resource.empty()) {
+                         if (meta_data->resource.empty()) {
                            return;
                          }
                          auto resource = this->apk_->LoadXml(meta_data->resource, diag);
@@ -2878,58 +2857,66 @@
   return true;
 }
 
+template <typename T>
+constexpr const char* GetExpectedTagForType() {
+  // This array does not appear at runtime, as GetExpectedTagForType function is used by compiler
+  // to inject proper 'expected_tag' into ElementCast.
+  std::array<std::pair<const char*, bool>, 37> tags = {
+      std::make_pair("action", std::is_same<Action, T>::value),
+      std::make_pair("activity", std::is_same<Activity, T>::value),
+      std::make_pair("additional-certificate", std::is_same<AdditionalCertificate, T>::value),
+      std::make_pair("application", std::is_same<Application, T>::value),
+      std::make_pair("category", std::is_same<Category, T>::value),
+      std::make_pair("compatible-screens", std::is_same<CompatibleScreens, T>::value),
+      std::make_pair("feature-group", std::is_same<FeatureGroup, T>::value),
+      std::make_pair("input-type", std::is_same<InputType, T>::value),
+      std::make_pair("intent-filter", std::is_same<IntentFilter, T>::value),
+      std::make_pair("meta-data", std::is_same<MetaData, T>::value),
+      std::make_pair("manifest", std::is_same<Manifest, T>::value),
+      std::make_pair("original-package", std::is_same<OriginalPackage, T>::value),
+      std::make_pair("overlay", std::is_same<Overlay, T>::value),
+      std::make_pair("package-verifier", std::is_same<PackageVerifier, T>::value),
+      std::make_pair("permission", std::is_same<Permission, T>::value),
+      std::make_pair("property", std::is_same<Property, T>::value),
+      std::make_pair("provider", std::is_same<Provider, T>::value),
+      std::make_pair("receiver", std::is_same<Receiver, T>::value),
+      std::make_pair("required-feature", std::is_same<RequiredFeature, T>::value),
+      std::make_pair("required-not-feature", std::is_same<RequiredNotFeature, T>::value),
+      std::make_pair("screen", std::is_same<Screen, T>::value),
+      std::make_pair("service", std::is_same<Service, T>::value),
+      std::make_pair("sdk-library", std::is_same<SdkLibrary, T>::value),
+      std::make_pair("static-library", std::is_same<StaticLibrary, T>::value),
+      std::make_pair("supports-gl-texture", std::is_same<SupportsGlTexture, T>::value),
+      std::make_pair("supports-input", std::is_same<SupportsInput, T>::value),
+      std::make_pair("supports-screens", std::is_same<SupportsScreen, T>::value),
+      std::make_pair("uses-configuration", std::is_same<UsesConfiguarion, T>::value),
+      std::make_pair("uses-feature", std::is_same<UsesFeature, T>::value),
+      std::make_pair("uses-library", std::is_same<UsesLibrary, T>::value),
+      std::make_pair("uses-native-library", std::is_same<UsesNativeLibrary, T>::value),
+      std::make_pair("uses-package", std::is_same<UsesPackage, T>::value),
+      std::make_pair("uses-permission", std::is_same<UsesPermission, T>::value),
+      std::make_pair("uses-permission-sdk-23", std::is_same<UsesPermissionSdk23, T>::value),
+      std::make_pair("uses-sdk", std::is_same<UsesSdkBadging, T>::value),
+      std::make_pair("uses-sdk-library", std::is_same<UsesSdkLibrary, T>::value),
+      std::make_pair("uses-static-library", std::is_same<UsesStaticLibrary, T>::value),
+  };
+  for (const auto& pair : tags) {
+    if (pair.second) {
+      return pair.first;
+    }
+  }
+  return nullptr;
+}
+
 /**
  * Returns the element casted to the type if the element is of that type. Otherwise, returns a null
  * pointer.
  **/
 template<typename T>
 T* ElementCast(ManifestExtractor::Element* element) {
-  if (element == nullptr) {
-    return nullptr;
-  }
-
-  const std::unordered_map<std::string, bool> kTagCheck = {
-      {"action", std::is_base_of<Action, T>::value},
-      {"activity", std::is_base_of<Activity, T>::value},
-      {"additional-certificate", std::is_base_of<AdditionalCertificate, T>::value},
-      {"application", std::is_base_of<Application, T>::value},
-      {"category", std::is_base_of<Category, T>::value},
-      {"compatible-screens", std::is_base_of<CompatibleScreens, T>::value},
-      {"feature-group", std::is_base_of<FeatureGroup, T>::value},
-      {"input-type", std::is_base_of<InputType, T>::value},
-      {"intent-filter", std::is_base_of<IntentFilter, T>::value},
-      {"meta-data", std::is_base_of<MetaData, T>::value},
-      {"manifest", std::is_base_of<Manifest, T>::value},
-      {"original-package", std::is_base_of<OriginalPackage, T>::value},
-      {"overlay", std::is_base_of<Overlay, T>::value},
-      {"package-verifier", std::is_base_of<PackageVerifier, T>::value},
-      {"permission", std::is_base_of<Permission, T>::value},
-      {"property", std::is_base_of<Property, T>::value},
-      {"provider", std::is_base_of<Provider, T>::value},
-      {"receiver", std::is_base_of<Receiver, T>::value},
-      {"required-feature", std::is_base_of<RequiredFeature, T>::value},
-      {"required-not-feature", std::is_base_of<RequiredNotFeature, T>::value},
-      {"screen", std::is_base_of<Screen, T>::value},
-      {"service", std::is_base_of<Service, T>::value},
-      {"sdk-library", std::is_base_of<SdkLibrary, T>::value},
-      {"static-library", std::is_base_of<StaticLibrary, T>::value},
-      {"supports-gl-texture", std::is_base_of<SupportsGlTexture, T>::value},
-      {"supports-input", std::is_base_of<SupportsInput, T>::value},
-      {"supports-screens", std::is_base_of<SupportsScreen, T>::value},
-      {"uses-configuration", std::is_base_of<UsesConfiguarion, T>::value},
-      {"uses-feature", std::is_base_of<UsesFeature, T>::value},
-      {"uses-library", std::is_base_of<UsesLibrary, T>::value},
-      {"uses-native-library", std::is_base_of<UsesNativeLibrary, T>::value},
-      {"uses-package", std::is_base_of<UsesPackage, T>::value},
-      {"uses-permission", std::is_base_of<UsesPermission, T>::value},
-      {"uses-permission-sdk-23", std::is_base_of<UsesPermissionSdk23, T>::value},
-      {"uses-sdk", std::is_base_of<UsesSdkBadging, T>::value},
-      {"uses-sdk-library", std::is_base_of<UsesSdkLibrary, T>::value},
-      {"uses-static-library", std::is_base_of<UsesStaticLibrary, T>::value},
-  };
-
-  auto check = kTagCheck.find(element->tag());
-  if (check != kTagCheck.end() && check->second) {
+  constexpr const char* expected_tag = GetExpectedTagForType<T>();
+  if (element != nullptr && expected_tag != nullptr && element->is_featured() &&
+      element->tag() == expected_tag) {
     return static_cast<T*>(element);
   }
   return nullptr;
@@ -2941,9 +2928,9 @@
 }
 
 std::unique_ptr<ManifestExtractor::Element> ManifestExtractor::Element::Inflate(
-    ManifestExtractor* extractor, xml::Element* el) {
-  const std::unordered_map<std::string,
-                           std::function<std::unique_ptr<ManifestExtractor::Element>()>>
+    ManifestExtractor* extractor, xml::Element* el, const std::string& parent_tag) {
+  static const std::unordered_map<std::string_view,
+                                  std::function<std::unique_ptr<ManifestExtractor::Element>()>>
       kTagCheck = {
           {"action", &CreateType<Action>},
           {"activity", &CreateType<Activity>},
@@ -2983,12 +2970,71 @@
           {"uses-sdk-library", &CreateType<UsesSdkLibrary>},
           {"uses-static-library", &CreateType<UsesStaticLibrary>},
       };
-
+  static constexpr std::array<std::pair<std::string_view, std::string_view>, 53>
+      kValidChildParentTags = {
+          std::make_pair("action", "intent-filter"),
+          std::make_pair("activity", "application"),
+          std::make_pair("additional-certificate", "uses-package"),
+          std::make_pair("additional-certificate", "uses-static-library"),
+          std::make_pair("application", "manifest"),
+          std::make_pair("category", "intent-filter"),
+          std::make_pair("compatible-screens", "manifest"),
+          std::make_pair("feature-group", "manifest"),
+          std::make_pair("input-type", "supports-input"),
+          std::make_pair("intent-filter", "activity"),
+          std::make_pair("intent-filter", "activity-alias"),
+          std::make_pair("intent-filter", "service"),
+          std::make_pair("intent-filter", "receiver"),
+          std::make_pair("intent-filter", "provider"),
+          std::make_pair("manifest", ""),
+          std::make_pair("meta-data", "activity"),
+          std::make_pair("meta-data", "activity-alias"),
+          std::make_pair("meta-data", "application"),
+          std::make_pair("meta-data", "service"),
+          std::make_pair("meta-data", "receiver"),
+          std::make_pair("meta-data", "provider"),
+          std::make_pair("original-package", "manifest"),
+          std::make_pair("overlay", "manifest"),
+          std::make_pair("package-verifier", "manifest"),
+          std::make_pair("permission", "manifest"),
+          std::make_pair("property", "activity"),
+          std::make_pair("property", "activity-alias"),
+          std::make_pair("property", "application"),
+          std::make_pair("property", "service"),
+          std::make_pair("property", "receiver"),
+          std::make_pair("property", "provider"),
+          std::make_pair("provider", "application"),
+          std::make_pair("receiver", "application"),
+          std::make_pair("required-feature", "uses-permission"),
+          std::make_pair("required-not-feature", "uses-permission"),
+          std::make_pair("screen", "compatible-screens"),
+          std::make_pair("service", "application"),
+          std::make_pair("sdk-library", "application"),
+          std::make_pair("static-library", "application"),
+          std::make_pair("supports-gl-texture", "manifest"),
+          std::make_pair("supports-input", "manifest"),
+          std::make_pair("supports-screens", "manifest"),
+          std::make_pair("uses-configuration", "manifest"),
+          std::make_pair("uses-feature", "feature-group"),
+          std::make_pair("uses-feature", "manifest"),
+          std::make_pair("uses-library", "application"),
+          std::make_pair("uses-native-library", "application"),
+          std::make_pair("uses-package", "application"),
+          std::make_pair("uses-permission", "manifest"),
+          std::make_pair("uses-permission-sdk-23", "manifest"),
+          std::make_pair("uses-sdk", "manifest"),
+          std::make_pair("uses-sdk-library", "application"),
+          std::make_pair("uses-static-library", "application"),
+      };
+  bool is_valid_tag = std::find(kValidChildParentTags.begin(), kValidChildParentTags.end(),
+                                std::make_pair<std::string_view, std::string_view>(
+                                    el->name, parent_tag)) != kValidChildParentTags.end();
   // Attempt to map the xml tag to a element inflater
   std::unique_ptr<ManifestExtractor::Element> element;
   auto check = kTagCheck.find(el->name);
-  if (check != kTagCheck.end()) {
+  if (check != kTagCheck.end() && is_valid_tag) {
     element = check->second();
+    element->featured_ = true;
   } else {
     element = util::make_unique<ManifestExtractor::Element>();
   }
@@ -2999,13 +3045,14 @@
   return element;
 }
 
-std::unique_ptr<ManifestExtractor::Element> ManifestExtractor::Visit(xml::Element* el) {
-  auto element = ManifestExtractor::Element::Inflate(this, el);
+std::unique_ptr<ManifestExtractor::Element> ManifestExtractor::Visit(
+    xml::Element* el, const std::string& parent_tag) {
+  auto element = ManifestExtractor::Element::Inflate(this, el, parent_tag);
   parent_stack_.insert(parent_stack_.begin(), element.get());
 
   // Process the element and recursively visit the children
   for (xml::Element* child : el->GetChildElements()) {
-    auto v = Visit(child);
+    auto v = Visit(child, el->name);
     element->AddChild(v);
   }
 
diff --git a/tools/aapt2/integration-tests/DumpTest/built_with_aapt.apk b/tools/aapt2/integration-tests/DumpTest/built_with_aapt.apk
new file mode 100644
index 0000000..090ebe56
--- /dev/null
+++ b/tools/aapt2/integration-tests/DumpTest/built_with_aapt.apk
Binary files differ
diff --git a/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt b/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt
new file mode 100644
index 0000000..cc0b3bf
--- /dev/null
+++ b/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt
@@ -0,0 +1,11 @@
+package: name='com.aapt.app' versionCode='222' versionName='222' platformBuildVersionName='12' platformBuildVersionCode='32' compileSdkVersion='32' compileSdkVersionCodename='12'
+sdkVersion:'22'
+targetSdkVersion:'32'
+application: label='App' icon=''
+feature-group: label=''
+  uses-feature: name='android.hardware.faketouch'
+  uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
+supports-screens: 'small' 'normal' 'large' 'xlarge'
+supports-any-density: 'true'
+locales:
+densities:
diff --git a/tools/aapt2/integration-tests/DumpTest/components_expected.txt b/tools/aapt2/integration-tests/DumpTest/components_expected.txt
index 79b6706..9c81fb8 100644
--- a/tools/aapt2/integration-tests/DumpTest/components_expected.txt
+++ b/tools/aapt2/integration-tests/DumpTest/components_expected.txt
@@ -36,6 +36,7 @@
 provides-component:'wallpaper'
 provides-component:'accessibility'
 provides-component:'print-service'
+provides-component:'payment'
 provides-component:'search'
 provides-component:'document-provider'
 provides-component:'notification-listener'
diff --git a/tools/aapt2/integration-tests/DumpTest/components_expected_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_expected_proto.txt
index 456a5a7..d866479 100644
--- a/tools/aapt2/integration-tests/DumpTest/components_expected_proto.txt
+++ b/tools/aapt2/integration-tests/DumpTest/components_expected_proto.txt
@@ -80,6 +80,7 @@
     provided_components: "wallpaper"
     provided_components: "accessibility"
     provided_components: "print-service"
+    provided_components: "payment"
     provided_components: "search"
     provided_components: "document-provider"
     provided_components: "notification-listener"
diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
index a7e8353..6da6fc6 100644
--- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
+++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
@@ -80,6 +80,7 @@
     provided_components: "wallpaper"
     provided_components: "accessibility"
     provided_components: "print-service"
+    provided_components: "payment"
     provided_components: "search"
     provided_components: "document-provider"
     provided_components: "notification-listener"
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
index bcef917..2665b3c 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
@@ -31,10 +31,11 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiArrayInitializerMemberValue
 import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiElement
 import com.intellij.psi.PsiMethod
 import org.jetbrains.uast.UAnnotation
-import org.jetbrains.uast.UClass
 import org.jetbrains.uast.UElement
 import org.jetbrains.uast.UMethod
 
@@ -65,6 +66,12 @@
         return listOf(UAnnotation::class.java)
     }
 
+    private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> {
+        if (elem is PsiArrayInitializerMemberValue)
+            return elem.getInitializers().map { it as PsiElement }.toTypedArray()
+        return elem.getChildren()
+    }
+
     private fun areAnnotationsEquivalent(
         context: JavaContext,
         anno1: PsiAnnotation,
@@ -82,18 +89,28 @@
             if (attr1[i].name != attr2[i].name) {
                 return false
             }
-            val value1 = attr1[i].value
-            val value2 = attr2[i].value
-            if (value1 == null && value2 == null) {
-                continue
-            }
-            if (value1 == null || value2 == null) {
-                return false
-            }
+            val value1 = attr1[i].value ?: return false
+            val value2 = attr2[i].value ?: return false
+            // Try to compare values directly with each other.
             val v1 = ConstantEvaluator.evaluate(context, value1)
             val v2 = ConstantEvaluator.evaluate(context, value2)
-            if (v1 != v2) {
-                return false
+            if (v1 != null && v2 != null) {
+                if (v1 != v2) {
+                    return false
+                }
+            } else {
+                val children1 = annotationValueGetChildren(value1)
+                val children2 = annotationValueGetChildren(value2)
+                if (children1.size != children2.size) {
+                    return false
+                }
+                for (j in children1.indices) {
+                    val c1 = ConstantEvaluator.evaluate(context, children1[j])
+                    val c2 = ConstantEvaluator.evaluate(context, children2[j])
+                    if (c1 != c2) {
+                        return false
+                    }
+                }
             }
         }
         return true
@@ -140,50 +157,13 @@
         }
     }
 
-    private fun compareClasses(
-        context: JavaContext,
-        element: UElement,
-        newClass: PsiClass,
-        extendedClass: PsiClass,
-        checkEquivalence: Boolean = true
-    ) {
-        val newAnnotation = newClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
-        val extendedAnnotation = extendedClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
-
-        val location = context.getLocation(element)
-        val newClassName = newClass.qualifiedName
-        val extendedClassName = extendedClass.qualifiedName
-        if (newAnnotation == null) {
-            val msg = "The class $newClassName extends the class $extendedClassName which " +
-                "is annotated with @EnforcePermission. The same annotation must be used " +
-                "on $newClassName."
-            context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
-        } else if (extendedAnnotation == null) {
-            val msg = "The class $newClassName extends the class $extendedClassName which " +
-                "is not annotated with @EnforcePermission. The same annotation must be used " +
-                "on $extendedClassName. Did you forget to annotate the AIDL definition?"
-            context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
-        } else if (checkEquivalence && !areAnnotationsEquivalent(
-            context, newAnnotation, extendedAnnotation)) {
-            val msg = "The class $newClassName is annotated with ${newAnnotation.text} " +
-                "which differs from the parent class $extendedClassName: " +
-                "${extendedAnnotation.text}. The same annotation must be used for " +
-                "both classes."
-            context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
-        }
-    }
-
     override fun visitAnnotationUsage(
         context: JavaContext,
         element: UElement,
         annotationInfo: AnnotationInfo,
         usageInfo: AnnotationUsageInfo
     ) {
-        if (usageInfo.type == AnnotationUsageType.EXTENDS) {
-            val newClass = element.sourcePsi?.parent?.parent as PsiClass
-            val extendedClass: PsiClass = usageInfo.referenced as PsiClass
-            compareClasses(context, element, newClass, extendedClass)
-        } else if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
+        if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
             annotationInfo.origin == AnnotationOrigin.METHOD) {
             val overridingMethod = element.sourcePsi as PsiMethod
             val overriddenMethod = usageInfo.referenced as PsiMethod
@@ -198,17 +178,7 @@
                     return
                 }
                 val method = node.uastParent as? UMethod
-                val klass = node.uastParent as? UClass
-                if (klass != null) {
-                    val newClass = klass as PsiClass
-                    val extendedClass = newClass.getSuperClass()
-                    if (extendedClass != null && extendedClass.qualifiedName != JAVA_OBJECT) {
-                        // The equivalence check can be skipped, if both classes are
-                        // annotated, it will be verified by visitAnnotationUsage.
-                        compareClasses(context, klass, newClass,
-                            extendedClass, checkEquivalence = false)
-                    }
-                } else if (method != null) {
+                if (method != null) {
                     val overridingMethod = method as PsiMethod
                     val parents = overridingMethod.findSuperMethods()
                     for (overriddenMethod in parents) {
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
index 618bbcc..4ed68a8 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
@@ -33,21 +33,6 @@
 
     override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
-    fun testDoesNotDetectIssuesCorrectAnnotationOnClass() {
-        lint().files(java(
-            """
-            package test.pkg;
-            @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
-            public class TestClass1 extends IFoo.Stub {
-                public void testMethod() {}
-            }
-            """).indented(),
-                *stubs
-        )
-        .run()
-        .expectClean()
-    }
-
     fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() {
         lint().files(java(
             """
@@ -65,26 +50,72 @@
         .expectClean()
     }
 
-    fun testDetectIssuesMismatchingAnnotationOnClass() {
+    fun testDoesNotDetectIssuesCorrectAnnotationAllOnMethod() {
         lint().files(java(
             """
             package test.pkg;
-            @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
-            public class TestClass3 extends IFoo.Stub {
-                public void testMethod() {}
+            import android.annotation.EnforcePermission;
+            public class TestClass11 extends IFooMethod.Stub {
+                @Override
+                @EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+                public void testMethodAll() {}
             }
             """).indented(),
                 *stubs
         )
         .run()
-        .expect("""src/test/pkg/TestClass3.java:3: Error: The class test.pkg.TestClass3 is \
-annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
-which differs from the parent class IFoo.Stub: \
-@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The \
-same annotation must be used for both classes. [MismatchingEnforcePermissionAnnotation]
-public class TestClass3 extends IFoo.Stub {
-                                ~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
+        .expectClean()
+    }
+
+    fun testDoesNotDetectIssuesCorrectAnnotationAllLiteralOnMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            import android.annotation.EnforcePermission;
+            public class TestClass111 extends IFooMethod.Stub {
+                @Override
+                @EnforcePermission(allOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE})
+                public void testMethodAllLiteral() {}
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expectClean()
+    }
+
+    fun testDoesNotDetectIssuesCorrectAnnotationAnyOnMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            import android.annotation.EnforcePermission;
+            public class TestClass12 extends IFooMethod.Stub {
+                @Override
+                @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+                public void testMethodAny() {}
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expectClean()
+    }
+
+    fun testDoesNotDetectIssuesCorrectAnnotationAnyLiteralOnMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            import android.annotation.EnforcePermission;
+            public class TestClass121 extends IFooMethod.Stub {
+                @Override
+                @EnforcePermission(anyOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE})
+                public void testMethodAnyLiteral() {}
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expectClean()
     }
 
     fun testDetectIssuesMismatchingAnnotationOnMethod() {
@@ -99,33 +130,132 @@
                 *stubs
         )
         .run()
-        .expect("""src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is \
-annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
-which differs from the overridden method Stub.testMethod: \
-@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same \
-annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
-    public void testMethod() {}
-                ~~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
+        .expect("""
+                src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
+                which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \
+                The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                    public void testMethod() {}
+                                ~~~~~~~~~~
+                1 errors, 0 warnings
+                """.addLineContinuation())
     }
 
-    fun testDetectIssuesMissingAnnotationOnClass() {
+    fun testDetectIssuesEmptyAnnotationOnMethod() {
         lint().files(java(
             """
             package test.pkg;
-            public class TestClass5 extends IFoo.Stub {
+            public class TestClass41 extends IFooMethod.Stub {
+                @android.annotation.EnforcePermission
                 public void testMethod() {}
             }
             """).indented(),
                 *stubs
         )
         .run()
-        .expect("""src/test/pkg/TestClass5.java:2: Error: The class test.pkg.TestClass5 extends \
-the class IFoo.Stub which is annotated with @EnforcePermission. The same annotation must be \
-used on test.pkg.TestClass5. [MissingEnforcePermissionAnnotation]
-public class TestClass5 extends IFoo.Stub {
-                                ~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
+        .expect("""
+                src/test/pkg/TestClass41.java:4: Error: The method TestClass41.testMethod is annotated with @android.annotation.EnforcePermission \
+                which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \
+                The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                    public void testMethod() {}
+                                ~~~~~~~~~~
+                1 errors, 0 warnings
+                """.addLineContinuation())
+    }
+
+    fun testDetectIssuesMismatchingAnyAnnotationOnMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            public class TestClass9 extends IFooMethod.Stub {
+                @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC})
+                public void testMethodAny() {}
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expect("""
+                src/test/pkg/TestClass9.java:4: Error: The method TestClass9.testMethodAny is annotated with \
+                @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \
+                which differs from the overridden method Stub.testMethodAny: \
+                @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \
+                The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                    public void testMethodAny() {}
+                                ~~~~~~~~~~~~~
+                1 errors, 0 warnings
+                """.addLineContinuation())
+    }
+
+    fun testDetectIssuesMismatchingAnyLiteralAnnotationOnMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            public class TestClass91 extends IFooMethod.Stub {
+                @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"})
+                public void testMethodAnyLiteral() {}
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expect("""
+                src/test/pkg/TestClass91.java:4: Error: The method TestClass91.testMethodAnyLiteral is annotated with \
+                @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \
+                which differs from the overridden method Stub.testMethodAnyLiteral: \
+                @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
+                The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                    public void testMethodAnyLiteral() {}
+                                ~~~~~~~~~~~~~~~~~~~~
+                1 errors, 0 warnings
+                """.addLineContinuation())
+    }
+
+    fun testDetectIssuesMismatchingAllAnnotationOnMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            public class TestClass10 extends IFooMethod.Stub {
+                @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC})
+                public void testMethodAll() {}
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expect("""
+                src/test/pkg/TestClass10.java:4: Error: The method TestClass10.testMethodAll is annotated with \
+                @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \
+                which differs from the overridden method Stub.testMethodAll: \
+                @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \
+                The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                    public void testMethodAll() {}
+                                ~~~~~~~~~~~~~
+                1 errors, 0 warnings
+                """.addLineContinuation())
+    }
+
+    fun testDetectIssuesMismatchingAllLiteralAnnotationOnMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            public class TestClass101 extends IFooMethod.Stub {
+                @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"})
+                public void testMethodAllLiteral() {}
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expect("""
+                src/test/pkg/TestClass101.java:4: Error: The method TestClass101.testMethodAllLiteral is annotated with \
+                @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \
+                which differs from the overridden method Stub.testMethodAllLiteral: \
+                @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
+                The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+                    public void testMethodAllLiteral() {}
+                                ~~~~~~~~~~~~~~~~~~~~
+                1 errors, 0 warnings
+                """.addLineContinuation())
     }
 
     fun testDetectIssuesMissingAnnotationOnMethod() {
@@ -139,12 +269,13 @@
                 *stubs
         )
         .run()
-        .expect("""src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod \
-overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same \
-annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation]
-    public void testMethod() {}
-                ~~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
+        .expect("""
+                src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod overrides the method Stub.testMethod which is annotated with @EnforcePermission. \
+                The same annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation]
+                    public void testMethod() {}
+                                ~~~~~~~~~~
+                1 errors, 0 warnings
+                """.addLineContinuation())
     }
 
     fun testDetectIssuesExtraAnnotationMethod() {
@@ -159,34 +290,13 @@
                 *stubs
         )
         .run()
-        .expect("""src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod \
-overrides the method Stub.testMethod which is not annotated with @EnforcePermission. The same \
-annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? \
-[MissingEnforcePermissionAnnotation]
-    public void testMethod() {}
-                ~~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
-    }
-
-    fun testDetectIssuesExtraAnnotationInterface() {
-        lint().files(java(
-            """
-            package test.pkg;
-            @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
-            public class TestClass8 extends IBar.Stub {
-                public void testMethod() {}
-            }
-            """).indented(),
-                *stubs
-        )
-        .run()
-        .expect("""src/test/pkg/TestClass8.java:2: Error: The class test.pkg.TestClass8 \
-extends the class IBar.Stub which is not annotated with @EnforcePermission. The same annotation \
-must be used on IBar.Stub. Did you forget to annotate the AIDL definition? \
-[MissingEnforcePermissionAnnotation]
-@android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
-^
-1 errors, 0 warnings""".addLineContinuation())
+        .expect("""
+                src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod overrides the method Stub.testMethod which is not annotated with @EnforcePermission. \
+                The same annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? [MissingEnforcePermissionAnnotation]
+                    public void testMethod() {}
+                                ~~~~~~~~~~
+                1 errors, 0 warnings
+                """.addLineContinuation())
     }
 
     fun testDetectIssuesMissingAnnotationOnMethodWhenClassIsCalledDefault() {
@@ -213,21 +323,6 @@
 
     /* Stubs */
 
-    // A service with permission annotation on the class.
-    private val interfaceIFooStub: TestFile = java(
-        """
-        @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
-        public interface IFoo {
-         @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
-         public static abstract class Stub extends android.os.Binder implements IFoo {
-           @Override
-           public void testMethod() {}
-         }
-         public void testMethod();
-        }
-        """
-    ).indented()
-
     // A service with permission annotation on the method.
     private val interfaceIFooMethodStub: TestFile = java(
         """
@@ -236,9 +331,29 @@
             @Override
             @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
             public void testMethod() {}
+            @Override
+            @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+            public void testMethodAny() {}
+            @Override
+            @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+            public void testMethodAnyLiteral() {}
+            @Override
+            @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+            public void testMethodAll() {}
+            @Override
+            @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+            public void testMethodAllLiteral() {}
           }
           @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
           public void testMethod();
+          @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+          public void testMethodAny() {}
+          @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+          public void testMethodAnyLiteral() {}
+          @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+          public void testMethodAll() {}
+          @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+          public void testMethodAllLiteral() {}
         }
         """
     ).indented()
@@ -261,6 +376,7 @@
         package android.Manifest;
         class permission {
           public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
+          public static final String NFC = "android.permission.NFC";
           public static final String INTERNET = "android.permission.INTERNET";
         }
         """
@@ -273,7 +389,7 @@
         """
     ).indented()
 
-    private val stubs = arrayOf(interfaceIFooStub, interfaceIFooMethodStub, interfaceIBarStub,
+    private val stubs = arrayOf(interfaceIFooMethodStub, interfaceIBarStub,
             manifestPermissionStub, enforcePermissionAnnotationStub)
 
     // Substitutes "backslash + new line" with an empty string to imitate line continuation