Merge "Require apps to be in forground to be able to scan"
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java b/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java
index dcabca4..727c682 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java
@@ -45,7 +45,6 @@
     private final int mTimeoutInSecond;
     private final Set<String> mActions;
 
-    private final Set<String> mActionReceivedForUser = new HashSet<>();
     private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>();
 
     private final Map<String, Semaphore> mSemaphoresMap = new ConcurrentHashMap<>();
@@ -80,7 +79,6 @@
                     final String data = intent.getDataString();
                     Log.d(mTag, "Received " + action + " for user " + userId
                             + (!TextUtils.isEmpty(data) ? " with " + data : ""));
-                    mActionReceivedForUser.add(action + userId);
                     getSemaphore(action, userId).release();
                 }
             }
@@ -95,10 +93,6 @@
         mBroadcastReceivers.forEach(mContext::unregisterReceiver);
     }
 
-    public boolean hasActionBeenReceivedForUser(String action, int userId) {
-        return mActionReceivedForUser.contains(action + userId);
-    }
-
     private boolean waitActionForUser(String action, int userId) {
         Log.d(mTag, "#waitActionForUser(action: " + action + ", userId: " + userId + ")");
 
@@ -129,7 +123,6 @@
     public String runThenWaitForBroadcasts(int userId, FunctionalUtils.ThrowingRunnable runnable,
             String... actions) {
         for (String action : actions) {
-            mActionReceivedForUser.remove(action + userId);
             getSemaphore(action, userId).drainPermits();
         }
         runnable.run();
@@ -140,9 +133,4 @@
         }
         return null;
     }
-
-    public boolean waitActionForUserIfNotReceivedYet(String action, int userId) {
-        return hasActionBeenReceivedForUser(action, userId)
-                || waitActionForUser(action, userId);
-    }
 }
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index b2bd8d7..3f9b54c 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -167,7 +167,7 @@
 
     /** Tests creating a new user. */
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
-    public void createUser() {
+    public void createUser() throws RemoteException {
         while (mRunner.keepRunning()) {
             Log.i(TAG, "Starting timer");
             final int userId = createUserNoFlags();
@@ -229,7 +229,7 @@
      * Measures the time until unlock listener is triggered and user is unlocked.
      */
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
-    public void startAndUnlockUser() {
+    public void startAndUnlockUser() throws RemoteException {
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
             final int userId = createUserNoFlags();
@@ -451,7 +451,7 @@
 
     /** Tests creating a new profile. */
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
-    public void managedProfileCreate() {
+    public void managedProfileCreate() throws RemoteException {
         assumeTrue(mHasManagedUserFeature);
 
         while (mRunner.keepRunning()) {
@@ -468,7 +468,7 @@
 
     /** Tests starting (unlocking) an uninitialized profile. */
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
-    public void managedProfileUnlock() {
+    public void managedProfileUnlock() throws RemoteException {
         assumeTrue(mHasManagedUserFeature);
 
         while (mRunner.keepRunning()) {
@@ -639,7 +639,7 @@
     // TODO: This is just a POC. Do this properly and add more.
     /** Tests starting (unlocking) a newly-created profile using the user-type-pkg-whitelist. */
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
-    public void managedProfileUnlock_usingWhitelist() {
+    public void managedProfileUnlock_usingWhitelist() throws RemoteException {
         assumeTrue(mHasManagedUserFeature);
         final int origMode = getUserTypePackageWhitelistMode();
         setUserTypePackageWhitelistMode(USER_TYPE_PACKAGE_WHITELIST_MODE_ENFORCE
@@ -665,7 +665,7 @@
     }
     /** Tests starting (unlocking) a newly-created profile NOT using the user-type-pkg-whitelist. */
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
-    public void managedProfileUnlock_notUsingWhitelist() {
+    public void managedProfileUnlock_notUsingWhitelist() throws RemoteException {
         assumeTrue(mHasManagedUserFeature);
         final int origMode = getUserTypePackageWhitelistMode();
         setUserTypePackageWhitelistMode(USER_TYPE_PACKAGE_WHITELIST_MODE_DISABLE);
@@ -908,10 +908,8 @@
                 result != null && result.contains("Failed"));
     }
 
-    private void removeUser(int userId) {
-        if (mBroadcastWaiter.hasActionBeenReceivedForUser(Intent.ACTION_USER_STARTED, userId)) {
-            mBroadcastWaiter.waitActionForUserIfNotReceivedYet(Intent.ACTION_MEDIA_MOUNTED, userId);
-        }
+    private void removeUser(int userId) throws RemoteException {
+        stopUserAfterWaitingForBroadcastIdle(userId, true);
         try {
             mUm.removeUser(userId);
             final long startTime = System.currentTimeMillis();
diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
index 78214dc..711caf7 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
@@ -391,6 +391,12 @@
      */
     public static final int REASON_MEDIA_NOTIFICATION_TRANSFER = 325;
 
+    /**
+     * Package installer.
+     * @hide
+     */
+    public static final int REASON_PACKAGE_INSTALLER = 326;
+
     /** @hide The app requests out-out. */
     public static final int REASON_OPT_OUT_REQUESTED = 1000;
 
@@ -472,6 +478,7 @@
             REASON_DISALLOW_APPS_CONTROL,
             REASON_ACTIVE_DEVICE_ADMIN,
             REASON_MEDIA_NOTIFICATION_TRANSFER,
+            REASON_PACKAGE_INSTALLER,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ReasonCode {}
@@ -839,6 +846,8 @@
                 return "REASON_OPT_OUT_REQUESTED";
             case REASON_MEDIA_NOTIFICATION_TRANSFER:
                 return "REASON_MEDIA_NOTIFICATION_TRANSFER";
+            case REASON_PACKAGE_INSTALLER:
+                return "REASON_PACKAGE_INSTALLER";
             default:
                 return "(unknown:" + reasonCode + ")";
         }
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 8defa16..4e52ed3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -4303,14 +4303,13 @@
 
     int getStorageSeq() {
         synchronized (mLock) {
-            return mStorageController != null ? mStorageController.getTracker().getSeq() : -1;
+            return mStorageController.getTracker().getSeq();
         }
     }
 
     boolean getStorageNotLow() {
         synchronized (mLock) {
-            return mStorageController != null
-                    ? mStorageController.getTracker().isStorageNotLow() : false;
+            return mStorageController.getTracker().isStorageNotLow();
         }
     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index 88270494..53c56e7 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -832,8 +832,8 @@
         private void writeConstraintsToXml(TypedXmlSerializer out, JobStatus jobStatus)
                 throws IOException {
             out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
+            final JobInfo job = jobStatus.getJob();
             if (jobStatus.hasConnectivityConstraint()) {
-                final JobInfo job = jobStatus.getJob();
                 final NetworkRequest network = jobStatus.getJob().getRequiredNetwork();
                 out.attribute(null, "net-capabilities-csv", intArrayToString(
                         network.getCapabilities()));
@@ -854,16 +854,16 @@
                             job.getMinimumNetworkChunkBytes());
                 }
             }
-            if (jobStatus.hasIdleConstraint()) {
+            if (job.isRequireDeviceIdle()) {
                 out.attribute(null, "idle", Boolean.toString(true));
             }
-            if (jobStatus.hasChargingConstraint()) {
+            if (job.isRequireCharging()) {
                 out.attribute(null, "charging", Boolean.toString(true));
             }
-            if (jobStatus.hasBatteryNotLowConstraint()) {
+            if (job.isRequireBatteryNotLow()) {
                 out.attribute(null, "battery-not-low", Boolean.toString(true));
             }
-            if (jobStatus.hasStorageNotLowConstraint()) {
+            if (job.isRequireStorageNotLow()) {
                 out.attribute(null, "storage-not-low", Boolean.toString(true));
             }
             out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
index 36a26f0..0a305a2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
@@ -273,6 +273,13 @@
                 return Integer.compare(job2.overrideState, job1.overrideState);
             }
 
+            final boolean job1UI = job1.getJob().isUserInitiated();
+            final boolean job2UI = job2.getJob().isUserInitiated();
+            if (job1UI != job2UI) {
+                // Attempt to run user-initiated jobs ahead of all other jobs.
+                return job1UI ? -1 : 1;
+            }
+
             final boolean job1EJ = job1.isRequestedExpeditedJob();
             final boolean job2EJ = job2.isRequestedExpeditedJob();
             if (job1EJ != job2EJ) {
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 b0b6a01..0b875cc 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
@@ -96,10 +96,16 @@
     public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
     public static final long NO_EARLIEST_RUNTIME = 0L;
 
-    static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0
-    static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;  // 1 << 2
-    static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
-    static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;  // 1 << 2
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public static final int CONSTRAINT_BATTERY_NOT_LOW =
+            JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public static final int CONSTRAINT_STORAGE_NOT_LOW =
+            JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
     public static final int CONSTRAINT_TIMING_DELAY = 1 << 31;
     public static final int CONSTRAINT_DEADLINE = 1 << 30;
     public static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
@@ -1820,7 +1826,8 @@
      * separately from the job's explicitly requested constraints and MUST be satisfied before
      * the job can run if the app doesn't have quota.
      */
-    private void addDynamicConstraints(int constraints) {
+    @VisibleForTesting
+    public void addDynamicConstraints(int constraints) {
         if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) {
             // Quota should never be used as a dynamic constraint.
             Slog.wtf(TAG, "Tried to set quota as a dynamic constraint");
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index b84c8a4..2b59209 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -164,8 +164,9 @@
     }
 
     @GuardedBy("mLock")
-    private boolean isAffordableLocked(long balance, long price, long ctp) {
-        return balance >= price && mScribe.getRemainingConsumableCakesLocked() >= ctp;
+    private boolean isAffordableLocked(long balance, long price, long stockLimitHonoringCtp) {
+        return balance >= price
+                && mScribe.getRemainingConsumableCakesLocked() >= stockLimitHonoringCtp;
     }
 
     @GuardedBy("mLock")
@@ -303,7 +304,8 @@
                         note.recalculateCosts(economicPolicy, userId, pkgName);
                         final boolean isAffordable = isVip
                                 || isAffordableLocked(newBalance,
-                                        note.getCachedModifiedPrice(), note.getCtp());
+                                        note.getCachedModifiedPrice(),
+                                        note.getStockLimitHonoringCtp());
                         if (note.isCurrentlyAffordable() != isAffordable) {
                             note.setNewAffordability(isAffordable);
                             mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -339,7 +341,7 @@
                 note.recalculateCosts(economicPolicy, userId, pkgName);
                 final boolean isAffordable = isVip
                         || isAffordableLocked(newBalance,
-                        note.getCachedModifiedPrice(), note.getCtp());
+                        note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp());
                 if (note.isCurrentlyAffordable() != isAffordable) {
                     note.setNewAffordability(isAffordable);
                     mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -403,7 +405,8 @@
                         note.recalculateCosts(economicPolicy, userId, pkgName);
                         final boolean isAffordable = isVip
                                 || isAffordableLocked(newBalance,
-                                        note.getCachedModifiedPrice(), note.getCtp());
+                                        note.getCachedModifiedPrice(),
+                                        note.getStockLimitHonoringCtp());
                         if (note.isCurrentlyAffordable() != isAffordable) {
                             note.setNewAffordability(isAffordable);
                             mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -541,7 +544,7 @@
                     final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
                     final boolean isAffordable = isVip
                             || isAffordableLocked(newBalance,
-                                    note.getCachedModifiedPrice(), note.getCtp());
+                                    note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp());
                     if (note.isCurrentlyAffordable() != isAffordable) {
                         note.setNewAffordability(isAffordable);
                         mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -882,7 +885,7 @@
                         mUpperThreshold = (mUpperThreshold == Long.MIN_VALUE)
                                 ? price : Math.min(mUpperThreshold, price);
                     }
-                    final long ctp = note.getCtp();
+                    final long ctp = note.getStockLimitHonoringCtp();
                     if (ctp <= mRemainingConsumableCredits) {
                         mCtpThreshold = Math.max(mCtpThreshold, ctp);
                     }
@@ -1119,7 +1122,7 @@
             note.recalculateCosts(economicPolicy, userId, pkgName);
             note.setNewAffordability(isVip
                     || isAffordableLocked(getBalanceLocked(userId, pkgName),
-                            note.getCachedModifiedPrice(), note.getCtp()));
+                            note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp()));
             mIrs.postAffordabilityChanged(userId, pkgName, note);
             // Update ongoing alarm
             scheduleBalanceCheckLocked(userId, pkgName);
@@ -1146,7 +1149,7 @@
     static final class ActionAffordabilityNote {
         private final EconomyManagerInternal.ActionBill mActionBill;
         private final EconomyManagerInternal.AffordabilityChangeListener mListener;
-        private long mCtp;
+        private long mStockLimitHonoringCtp;
         private long mModifiedPrice;
         private boolean mIsAffordable;
 
@@ -1185,29 +1188,34 @@
             return mModifiedPrice;
         }
 
-        private long getCtp() {
-            return mCtp;
+        /** Returns the cumulative CTP of actions in this note that respect the stock limit. */
+        private long getStockLimitHonoringCtp() {
+            return mStockLimitHonoringCtp;
         }
 
         @VisibleForTesting
         void recalculateCosts(@NonNull EconomicPolicy economicPolicy,
                 int userId, @NonNull String pkgName) {
             long modifiedPrice = 0;
-            long ctp = 0;
+            long stockLimitHonoringCtp = 0;
             final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions =
                     mActionBill.getAnticipatedActions();
             for (int i = 0; i < anticipatedActions.size(); ++i) {
                 final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i);
+                final EconomicPolicy.Action action = economicPolicy.getAction(aa.actionId);
 
                 final EconomicPolicy.Cost actionCost =
                         economicPolicy.getCostOfAction(aa.actionId, userId, pkgName);
                 modifiedPrice += actionCost.price * aa.numInstantaneousCalls
                         + actionCost.price * (aa.ongoingDurationMs / 1000);
-                ctp += actionCost.costToProduce * aa.numInstantaneousCalls
-                        + actionCost.costToProduce * (aa.ongoingDurationMs / 1000);
+                if (action.respectsStockLimit) {
+                    stockLimitHonoringCtp +=
+                            actionCost.costToProduce * aa.numInstantaneousCalls
+                                    + actionCost.costToProduce * (aa.ongoingDurationMs / 1000);
+                }
             }
             mModifiedPrice = modifiedPrice;
-            mCtp = ctp;
+            mStockLimitHonoringCtp = stockLimitHonoringCtp;
         }
 
         boolean isCurrentlyAffordable() {
@@ -1267,7 +1275,8 @@
                                 final ActionAffordabilityNote note =
                                         actionAffordabilityNotes.valueAt(i);
                                 final boolean isAffordable = isVip || isAffordableLocked(
-                                        newBalance, note.getCachedModifiedPrice(), note.getCtp());
+                                        newBalance, note.getCachedModifiedPrice(),
+                                        note.getStockLimitHonoringCtp());
                                 if (note.isCurrentlyAffordable() != isAffordable) {
                                     note.setNewAffordability(isAffordable);
                                     mIrs.postAffordabilityChanged(userId, pkgName, note);
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
index 46338fa..a6d064c 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
@@ -262,12 +262,17 @@
                 KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE,
                 DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE_CAKES);
 
+        // Apps must hold the SCHEDULE_EXACT_ALARM or USE_EXACT_ALARMS permission in order to use
+        // exact alarms. Since the user has the option of granting/revoking the permission, we can
+        // be a little lenient on the action cost checks and only stop the action if the app has
+        // run out of credits, and not when the system has run out of stock.
         mActions.put(ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE,
                 new Action(ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE,
                         getConstantAsCake(mParser, properties,
                                 KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_CTP,
                                 DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_CTP_CAKES),
-                        exactAllowWhileIdleWakeupBasePrice));
+                        exactAllowWhileIdleWakeupBasePrice,
+                        /* respectsStockLimit */ false));
         mActions.put(ACTION_ALARM_WAKEUP_EXACT,
                 new Action(ACTION_ALARM_WAKEUP_EXACT,
                         getConstantAsCake(mParser, properties,
@@ -275,7 +280,8 @@
                                 DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_CTP_CAKES),
                         getConstantAsCake(mParser, properties,
                                 KEY_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE,
-                                DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE_CAKES)));
+                                DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE_CAKES),
+                        /* respectsStockLimit */ false));
 
         final long inexactAllowWhileIdleWakeupBasePrice =
                 getConstantAsCake(mParser, properties,
@@ -287,7 +293,8 @@
                         getConstantAsCake(mParser, properties,
                                 KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_WAKEUP_CTP,
                                 DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_WAKEUP_CTP_CAKES),
-                        inexactAllowWhileIdleWakeupBasePrice));
+                        inexactAllowWhileIdleWakeupBasePrice,
+                        /* respectsStockLimit */ false));
         mActions.put(ACTION_ALARM_WAKEUP_INEXACT,
                 new Action(ACTION_ALARM_WAKEUP_INEXACT,
                         getConstantAsCake(mParser, properties,
@@ -295,7 +302,8 @@
                                 DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP_CAKES),
                         getConstantAsCake(mParser, properties,
                                 KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE,
-                                DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES)));
+                                DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES),
+                        /* respectsStockLimit */ false));
 
         final long exactAllowWhileIdleNonWakeupBasePrice = getConstantAsCake(mParser, properties,
                 KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_BASE_PRICE,
@@ -305,7 +313,8 @@
                         getConstantAsCake(mParser, properties,
                                 KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_CTP,
                                 DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_CTP_CAKES),
-                        exactAllowWhileIdleNonWakeupBasePrice));
+                        exactAllowWhileIdleNonWakeupBasePrice,
+                        /* respectsStockLimit */ false));
 
         mActions.put(ACTION_ALARM_NONWAKEUP_EXACT,
                 new Action(ACTION_ALARM_NONWAKEUP_EXACT,
@@ -314,7 +323,8 @@
                                 DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_CTP_CAKES),
                         getConstantAsCake(mParser, properties,
                                 KEY_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE,
-                                DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE_CAKES)));
+                                DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE_CAKES),
+                        /* respectsStockLimit */ false));
 
         final long inexactAllowWhileIdleNonWakeupBasePrice = getConstantAsCake(mParser, properties,
                 KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_NONWAKEUP_BASE_PRICE,
@@ -342,7 +352,8 @@
                                 DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_CTP_CAKES),
                         getConstantAsCake(mParser, properties,
                                 KEY_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE,
-                                DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE_CAKES)));
+                                DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE_CAKES),
+                        /* respectsStockLimit */ false));
 
         mRewards.put(REWARD_TOP_ACTIVITY, new Reward(REWARD_TOP_ACTIVITY,
                 getConstantAsCake(mParser, properties,
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
index b52f6f1..a4043dd 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
@@ -149,11 +149,22 @@
          * the action unless a modifier lowers the cost to produce.
          */
         public final long basePrice;
+        /**
+         * Whether the remaining stock limit affects an app's ability to perform this action.
+         * If {@code false}, then the action can be performed, even if the cost is higher
+         * than the remaining stock. This does not affect checking against an app's balance.
+         */
+        public final boolean respectsStockLimit;
 
         Action(int id, long costToProduce, long basePrice) {
+            this(id, costToProduce, basePrice, true);
+        }
+
+        Action(int id, long costToProduce, long basePrice, boolean respectsStockLimit) {
             this.id = id;
             this.costToProduce = costToProduce;
             this.basePrice = basePrice;
+            this.respectsStockLimit = respectsStockLimit;
         }
     }
 
diff --git a/core/api/current.txt b/core/api/current.txt
index 1b0c5c2..5e6417c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -122,6 +122,10 @@
     field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS";
     field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE";
     field public static final String MANAGE_DEVICE_LOCK_STATE = "android.permission.MANAGE_DEVICE_LOCK_STATE";
+    field public static final String MANAGE_DEVICE_POLICY_ACROSS_USERS = "android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS";
+    field public static final String MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL = "android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL";
+    field public static final String MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL = "android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL";
+    field public static final String MANAGE_DEVICE_POLICY_TIME = "android.permission.MANAGE_DEVICE_POLICY_TIME";
     field public static final String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS";
     field public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE";
     field public static final String MANAGE_MEDIA = "android.permission.MANAGE_MEDIA";
@@ -7499,9 +7503,9 @@
     method @Nullable public String getAlwaysOnVpnPackage(@NonNull android.content.ComponentName);
     method @NonNull @WorkerThread public android.os.Bundle getApplicationRestrictions(@Nullable android.content.ComponentName, String);
     method @Deprecated @Nullable public String getApplicationRestrictionsManagingPackage(@NonNull android.content.ComponentName);
-    method public boolean getAutoTimeEnabled(@NonNull android.content.ComponentName);
+    method @RequiresPermission(anyOf={android.Manifest.permission.SET_TIME, "android.permission.QUERY_ADMIN_POLICY"}, conditional=true) public boolean getAutoTimeEnabled(@NonNull android.content.ComponentName);
     method @Deprecated public boolean getAutoTimeRequired();
-    method public boolean getAutoTimeZoneEnabled(@NonNull android.content.ComponentName);
+    method @RequiresPermission(anyOf={android.Manifest.permission.SET_TIME_ZONE, "android.permission.QUERY_ADMIN_POLICY"}, conditional=true) public boolean getAutoTimeZoneEnabled(@NonNull android.content.ComponentName);
     method @NonNull public java.util.List<android.os.UserHandle> getBindDeviceAdminTargetUsers(@NonNull android.content.ComponentName);
     method public boolean getBluetoothContactSharingDisabled(@NonNull android.content.ComponentName);
     method public boolean getCameraDisabled(@Nullable android.content.ComponentName);
@@ -7648,9 +7652,9 @@
     method public boolean setApplicationHidden(@NonNull android.content.ComponentName, String, boolean);
     method @WorkerThread public void setApplicationRestrictions(@Nullable android.content.ComponentName, String, android.os.Bundle);
     method @Deprecated public void setApplicationRestrictionsManagingPackage(@NonNull android.content.ComponentName, @Nullable String) throws android.content.pm.PackageManager.NameNotFoundException;
-    method public void setAutoTimeEnabled(@NonNull android.content.ComponentName, boolean);
+    method @RequiresPermission(value=android.Manifest.permission.SET_TIME, conditional=true) public void setAutoTimeEnabled(@NonNull android.content.ComponentName, boolean);
     method @Deprecated public void setAutoTimeRequired(@NonNull android.content.ComponentName, boolean);
-    method public void setAutoTimeZoneEnabled(@NonNull android.content.ComponentName, boolean);
+    method @RequiresPermission(value=android.Manifest.permission.SET_TIME_ZONE, conditional=true) public void setAutoTimeZoneEnabled(@NonNull android.content.ComponentName, boolean);
     method public void setBackupServiceEnabled(@NonNull android.content.ComponentName, boolean);
     method public void setBluetoothContactSharingDisabled(@NonNull android.content.ComponentName, boolean);
     method public void setCameraDisabled(@NonNull android.content.ComponentName, boolean);
@@ -7729,8 +7733,8 @@
     method @Deprecated public int setStorageEncryption(@NonNull android.content.ComponentName, boolean);
     method public void setSystemSetting(@NonNull android.content.ComponentName, @NonNull String, String);
     method public void setSystemUpdatePolicy(@NonNull android.content.ComponentName, android.app.admin.SystemUpdatePolicy);
-    method public boolean setTime(@NonNull android.content.ComponentName, long);
-    method public boolean setTimeZone(@NonNull android.content.ComponentName, String);
+    method @RequiresPermission(value=android.Manifest.permission.SET_TIME, conditional=true) public boolean setTime(@NonNull android.content.ComponentName, long);
+    method @RequiresPermission(value=android.Manifest.permission.SET_TIME_ZONE, conditional=true) public boolean setTimeZone(@NonNull android.content.ComponentName, String);
     method public void setTrustAgentConfiguration(@NonNull android.content.ComponentName, @NonNull android.content.ComponentName, android.os.PersistableBundle);
     method public void setUninstallBlocked(@Nullable android.content.ComponentName, String, boolean);
     method public void setUsbDataSignalingEnabled(boolean);
@@ -7803,7 +7807,7 @@
     field @Deprecated public static final String EXTRA_PROVISIONING_EMAIL_ADDRESS = "android.app.extra.PROVISIONING_EMAIL_ADDRESS";
     field public static final String EXTRA_PROVISIONING_IMEI = "android.app.extra.PROVISIONING_IMEI";
     field public static final String EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION = "android.app.extra.PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION";
-    field public static final String EXTRA_PROVISIONING_KEEP_SCREEN_ON = "android.app.extra.PROVISIONING_KEEP_SCREEN_ON";
+    field @Deprecated public static final String EXTRA_PROVISIONING_KEEP_SCREEN_ON = "android.app.extra.PROVISIONING_KEEP_SCREEN_ON";
     field public static final String EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED = "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED";
     field public static final String EXTRA_PROVISIONING_LOCALE = "android.app.extra.PROVISIONING_LOCALE";
     field public static final String EXTRA_PROVISIONING_LOCAL_TIME = "android.app.extra.PROVISIONING_LOCAL_TIME";
@@ -40318,12 +40322,14 @@
 
   public final class Tile implements android.os.Parcelable {
     method public int describeContents();
+    method @Nullable public android.app.PendingIntent getActivityLaunchForClick();
     method public CharSequence getContentDescription();
     method public android.graphics.drawable.Icon getIcon();
     method public CharSequence getLabel();
     method public int getState();
     method @Nullable public CharSequence getStateDescription();
     method @Nullable public CharSequence getSubtitle();
+    method public void setActivityLaunchForClick(@Nullable android.app.PendingIntent);
     method public void setContentDescription(CharSequence);
     method public void setIcon(android.graphics.drawable.Icon);
     method public void setLabel(CharSequence);
@@ -40352,6 +40358,7 @@
     method public static final void requestListeningState(android.content.Context, android.content.ComponentName);
     method public final void showDialog(android.app.Dialog);
     method public final void startActivityAndCollapse(android.content.Intent);
+    method public final void startActivityAndCollapse(@NonNull android.app.PendingIntent);
     method public final void unlockAndRun(Runnable);
     field public static final String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE";
     field public static final String ACTION_QS_TILE_PREFERENCES = "android.service.quicksettings.action.QS_TILE_PREFERENCES";
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 2546294..447b113 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -82,6 +82,7 @@
   }
 
   public abstract class Context {
+    method @NonNull public android.os.IBinder getIApplicationThreadBinder();
     method @NonNull public android.os.UserHandle getUser();
     field public static final String PAC_PROXY_SERVICE = "pac_proxy";
     field public static final String TEST_NETWORK_SERVICE = "test_network";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 712ac94..b4594e0 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6607,6 +6607,7 @@
   public class AudioManager {
     method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addAssistantServicesUids(@NonNull int[]);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void addOnDevicesForAttributesChangedListener(@NonNull android.media.AudioAttributes, @NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnNonDefaultDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener) throws java.lang.SecurityException;
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForCapturePresetChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener) throws java.lang.SecurityException;
@@ -6647,6 +6648,7 @@
     method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void removeOnDevicesForAttributesChangedListener(@NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnNonDefaultDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForCapturePresetChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener);
@@ -6704,6 +6706,10 @@
     field public static final int EVENT_TIMEOUT = 2; // 0x2
   }
 
+  public static interface AudioManager.OnDevicesForAttributesChangedListener {
+    method public void onDevicesForAttributesChanged(@NonNull android.media.AudioAttributes, @NonNull java.util.List<android.media.AudioDeviceAttributes>);
+  }
+
   public static interface AudioManager.OnNonDefaultDevicesForStrategyChangedListener {
     method public void onNonDefaultDevicesForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull java.util.List<android.media.AudioDeviceAttributes>);
   }
@@ -12539,6 +12545,19 @@
     field public static final int INITIALIZATION_STATUS_UNKNOWN = 100; // 0x64
   }
 
+  public abstract class VisualQueryDetectionService extends android.app.Service implements android.service.voice.SandboxedDetectionServiceBase {
+    ctor public VisualQueryDetectionService();
+    method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
+    method public void onStartDetection(@NonNull android.service.voice.VisualQueryDetectionService.Callback);
+    method public void onStopDetection();
+    method public void onUpdateState(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, long, @Nullable java.util.function.IntConsumer);
+    field public static final String SERVICE_INTERFACE = "android.service.voice.VisualQueryDetectionService";
+  }
+
+  public static final class VisualQueryDetectionService.Callback {
+    ctor public VisualQueryDetectionService.Callback();
+  }
+
   public class VoiceInteractionService extends android.app.Service {
     method @NonNull public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback);
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(String, java.util.Locale, @Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, android.service.voice.AlwaysOnHotwordDetector.Callback);
diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java
index 871d15e..51ea04f 100644
--- a/core/java/android/app/ApplicationExitInfo.java
+++ b/core/java/android/app/ApplicationExitInfo.java
@@ -416,6 +416,15 @@
      */
     public static final int SUBREASON_UNDELIVERED_BROADCAST = 26;
 
+    /**
+     * The process was killed because its associated SDK sandbox process (where it had loaded SDKs)
+     * had died; this would be set only when the reason is {@link #REASON_DEPENDENCY_DIED}.
+     *
+     * For internal use only.
+     * @hide
+     */
+    public static final int SUBREASON_SDK_SANDBOX_DIED = 27;
+
     // If there is any OEM code which involves additional app kill reasons, it should
     // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000.
 
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 1120257..240dbe1 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -2028,6 +2028,13 @@
     }
 
     /** @hide */
+    @NonNull
+    @Override
+    public IBinder getIApplicationThreadBinder() {
+        return getIApplicationThread().asBinder();
+    }
+
+    /** @hide */
     @Override
     public Handler getMainThreadHandler() {
         return mMainThread.getHandler();
@@ -3039,7 +3046,8 @@
     public void updateDeviceId(int updatedDeviceId) {
         if (!isValidDeviceId(updatedDeviceId)) {
             throw new IllegalArgumentException(
-                    "Not a valid ID of the default device or any virtual device: " + mDeviceId);
+                    "Not a valid ID of the default device or any virtual device: "
+                            + updatedDeviceId);
         }
         if (mIsExplicitDeviceId) {
             throw new UnsupportedOperationException(
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index e729e7d..209b112 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -16,6 +16,9 @@
 
 package android.app.admin;
 
+import static android.Manifest.permission.QUERY_ADMIN_POLICY;
+import static android.Manifest.permission.SET_TIME;
+import static android.Manifest.permission.SET_TIME_ZONE;
 import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 
@@ -3311,13 +3314,16 @@
      * A {@code boolean} flag that indicates whether the screen should be on throughout the
      * provisioning flow.
      *
-     * <p>The default value is {@code false}.
-     *
      * <p>This extra can either be passed as an extra to the {@link
      * #ACTION_PROVISION_MANAGED_PROFILE} intent, or it can be returned by the
      * admin app when performing the admin-integrated provisioning flow as a result of the
      * {@link #ACTION_GET_PROVISIONING_MODE} activity.
+     *
+     * @deprecated from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the flag wouldn't
+     * be functional. The screen is kept on throughout the provisioning flow.
      */
+
+    @Deprecated
     public static final String EXTRA_PROVISIONING_KEEP_SCREEN_ON =
             "android.app.extra.PROVISIONING_KEEP_SCREEN_ON";
 
@@ -8466,8 +8472,10 @@
     }
 
     /**
-     * Called by a device owner, a profile owner for the primary user or a profile
-     * owner of an organization-owned managed profile to turn auto time on and off.
+     * Called by a device owner, a profile owner for the primary user, a profile
+     * owner of an organization-owned managed profile or, starting from Android
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, holders of the permission
+     * {@link android.Manifest.permission#SET_TIME} to turn auto time on and off.
      * Callers are recommended to use {@link UserManager#DISALLOW_CONFIG_DATE_TIME}
      * to prevent the user from changing this setting.
      * <p>
@@ -8478,8 +8486,10 @@
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
      * @param enabled Whether time should be obtained automatically from the network or not.
      * @throws SecurityException if caller is not a device owner, a profile owner for the
-     * primary user, or a profile owner of an organization-owned managed profile.
+     * primary user, or a profile owner of an organization-owned managed profile or a holder of the
+     * permission {@link android.Manifest.permission#SET_TIME}.
      */
+    @RequiresPermission(value = SET_TIME, conditional = true)
     public void setAutoTimeEnabled(@NonNull ComponentName admin, boolean enabled) {
         if (mService != null) {
             try {
@@ -8491,10 +8501,18 @@
     }
 
     /**
+     * Returns true if auto time is enabled on the device.
+     *
+     * <p> Starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, callers
+     * are also able to call this method if they hold the permission
+     *{@link android.Manifest.permission#SET_TIME}.
+     *
      * @return true if auto time is enabled on the device.
-     * @throws SecurityException if caller is not a device owner, a profile owner for the
-     * primary user, or a profile owner of an organization-owned managed profile.
+     * @throws SecurityException if the caller is not a device owner, a profile
+     * owner for the primary user, or a profile owner of an organization-owned managed profile or a
+     * holder of the permission {@link android.Manifest.permission#SET_TIME}.
      */
+    @RequiresPermission(anyOf = {SET_TIME, QUERY_ADMIN_POLICY}, conditional = true)
     public boolean getAutoTimeEnabled(@NonNull ComponentName admin) {
         if (mService != null) {
             try {
@@ -8507,8 +8525,10 @@
     }
 
     /**
-     * Called by a device owner, a profile owner for the primary user or a profile
-     * owner of an organization-owned managed profile to turn auto time zone on and off.
+     * Called by a device owner, a profile owner for the primary user, a profile
+     * owner of an organization-owned managed profile or, starting from Android
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, holders of the permission
+     * {@link android.Manifest.permission#SET_TIME} to turn auto time zone on and off.
      * Callers are recommended to use {@link UserManager#DISALLOW_CONFIG_DATE_TIME}
      * to prevent the user from changing this setting.
      * <p>
@@ -8519,8 +8539,10 @@
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
      * @param enabled Whether time zone should be obtained automatically from the network or not.
      * @throws SecurityException if caller is not a device owner, a profile owner for the
-     * primary user, or a profile owner of an organization-owned managed profile.
+     * primary user, or a profile owner of an organization-owned managed profile or a holder of the
+     * permission {@link android.Manifest.permission#SET_TIME_ZONE}.
      */
+    @RequiresPermission(value = SET_TIME_ZONE, conditional = true)
     public void setAutoTimeZoneEnabled(@NonNull ComponentName admin, boolean enabled) {
         throwIfParentInstance("setAutoTimeZone");
         if (mService != null) {
@@ -8533,10 +8555,18 @@
     }
 
     /**
+     * Returns true if auto time zone is enabled on the device.
+     *
+     * <p> Starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, callers
+     * are also able to call this method if they hold the permission
+     *{@link android.Manifest.permission#SET_TIME}.
+     *
      * @return true if auto time zone is enabled on the device.
-     * @throws SecurityException if caller is not a device owner, a profile owner for the
-     * primary user, or a profile owner of an organization-owned managed profile.
+     * @throws SecurityException if the caller is not a device owner, a profile
+     * owner for the primary user, or a profile owner of an organization-owned managed profile or a
+     * holder of the permission {@link android.Manifest.permission#SET_TIME_ZONE}.
      */
+    @RequiresPermission(anyOf = {SET_TIME_ZONE, QUERY_ADMIN_POLICY}, conditional = true)
     public boolean getAutoTimeZoneEnabled(@NonNull ComponentName admin) {
         throwIfParentInstance("getAutoTimeZone");
         if (mService != null) {
@@ -11875,17 +11905,21 @@
     }
 
     /**
-     * Called by a device owner or a profile owner of an organization-owned managed
-     * profile to set the system wall clock time. This only takes effect if called when
-     * {@link android.provider.Settings.Global#AUTO_TIME} is 0, otherwise {@code false}
-     * will be returned.
+     * Called by a device owner, a profile owner of an organization-owned managed
+     * profile or, starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+     * holders of the permission {@link android.Manifest.permission#SET_TIME} to set the system wall
+     * clock time. This only takes effect if called when
+     * {@link android.provider.Settings.Global#AUTO_TIME} is 0, otherwise {@code false} will be
+     * returned.
      *
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with
      * @param millis time in milliseconds since the Epoch
      * @return {@code true} if set time succeeded, {@code false} otherwise.
      * @throws SecurityException if {@code admin} is not a device owner or a profile owner
-     * of an organization-owned managed profile.
+     * of an organization-owned managed profile or a holder of the permission
+     * {@link android.Manifest.permission#SET_TIME}.
      */
+    @RequiresPermission(value = SET_TIME, conditional = true)
     public boolean setTime(@NonNull ComponentName admin, long millis) {
         throwIfParentInstance("setTime");
         if (mService != null) {
@@ -11899,10 +11933,12 @@
     }
 
     /**
-     * Called by a device owner or a profile owner of an organization-owned managed
-     * profile to set the system's persistent default time zone. This only takes
-     * effect if called when {@link android.provider.Settings.Global#AUTO_TIME_ZONE}
-     * is 0, otherwise {@code false} will be returned.
+     * Called by a device owner, a profile owner of an organization-owned managed
+     * profile or, starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+     * holders of the permission {@link android.Manifest.permission#SET_TIME_ZONE} to set the
+     * system's persistent default time zone. This only take effect if called when
+     * {@link android.provider.Settings.Global#AUTO_TIME_ZONE} is 0, otherwise {@code false} will be
+     * returned.
      *
      * @see android.app.AlarmManager#setTimeZone(String)
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with
@@ -11910,8 +11946,10 @@
      *     {@link java.util.TimeZone#getAvailableIDs}
      * @return {@code true} if set timezone succeeded, {@code false} otherwise.
      * @throws SecurityException if {@code admin} is not a device owner or a profile owner
-     * of an organization-owned managed profile.
+     * of an organization-owned managed profile or a holder of the permissions
+     * {@link android.Manifest.permission#SET_TIME_ZONE}.
      */
+    @RequiresPermission(value = SET_TIME_ZONE, conditional = true)
     public boolean setTimeZone(@NonNull ComponentName admin, String timeZone) {
         throwIfParentInstance("setTimeZone");
         if (mService != null) {
diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java
index 840f3a3..3a61ca1 100644
--- a/core/java/android/app/admin/DevicePolicyManagerInternal.java
+++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java
@@ -271,6 +271,31 @@
     public abstract void resetOp(int op, String packageName, @UserIdInt int userId);
 
     /**
+     * Checks if the calling process has been granted permission to apply a device policy on a
+     * specific user.
+     *
+     * The given permission will be checked along with its associated cross-user permission, if it
+     * exists and the target user is different to the calling user.
+     *
+     * @param permission The name of the permission being checked.
+     * @param targetUserId The userId of the user which the caller needs permission to act on.
+     * @throws SecurityException If the calling process has not been granted the permission.
+     */
+    public abstract void enforcePermission(String permission, int targetUserId);
+
+    /**
+     * Return whether the calling process has been granted permission to apply a device policy on
+     * a specific user.
+     *
+     * The given permission will be checked along with its associated cross-user
+     * permission, if it exists and the target user is different to the calling user.
+     *
+     * @param permission The name of the permission being checked.
+     * @param targetUserId The userId of the user which the caller needs permission to act on.
+     */
+    public abstract boolean hasPermission(String permission, int targetUserId);
+
+    /**
      * Returns whether new "turn off work" behavior is enabled via feature flag.
      */
     public abstract boolean isKeepProfilesRunningEnabled();
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 7d7232e..20b0626 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -7491,6 +7491,18 @@
     }
 
     /**
+     * Get the binder object associated with the IApplicationThread of this Context.
+     *
+     * This can be used by a mainline module to uniquely identify a specific app process.
+     * @hide
+     */
+    @NonNull
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public IBinder getIApplicationThreadBinder() {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
      * @hide
      */
     public Handler getMainThreadHandler() {
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 0a32dd78..0dc4adc 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -1277,6 +1277,14 @@
      * @hide
      */
     @Override
+    public IBinder getIApplicationThreadBinder() {
+        return mBase.getIApplicationThreadBinder();
+    }
+
+    /**
+     * @hide
+     */
+    @Override
     public Handler getMainThreadHandler() {
         return mBase.getMainThreadHandler();
     }
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index c5ebf34..a9f55bc 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1066,6 +1066,19 @@
     public @interface SizeChangesSupportMode {}
 
     /**
+     * This change id enables compat policy that ignores app requested orientation in
+     * response to an app calling {@link android.app.Activity#setRequestedOrientation}. See
+     * com.android.server.wm.LetterboxUiController#shouldIgnoreRequestedOrientation for
+     * details.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION =
+            254631730L; // buganizer id
+
+    /**
      * This change id forces the packages it is applied to never have Display API sandboxing
      * applied for a letterbox or SCM activity. The Display APIs will continue to provide
      * DisplayArea bounds.
diff --git a/core/java/android/service/quicksettings/Tile.java b/core/java/android/service/quicksettings/Tile.java
index 289b0e0..910fc44 100644
--- a/core/java/android/service/quicksettings/Tile.java
+++ b/core/java/android/service/quicksettings/Tile.java
@@ -227,7 +227,6 @@
 
     /**
      * Gets the Activity {@link PendingIntent} to be launched when the tile is clicked.
-     * @hide
      */
     @Nullable
     public PendingIntent getActivityLaunchForClick() {
@@ -243,7 +242,6 @@
      * (This is the default behavior if this method is never called.)
      * @param pendingIntent a PendingIntent for an activity to be launched onclick, or {@code null}
      *                      to handle the clicks in the `TileService`.
-     * @hide
      */
     public void setActivityLaunchForClick(@Nullable PendingIntent pendingIntent) {
         if (pendingIntent != null && !pendingIntent.isActivity()) {
diff --git a/core/java/android/service/quicksettings/TileService.java b/core/java/android/service/quicksettings/TileService.java
index 506b3b8..7b6ff97 100644
--- a/core/java/android/service/quicksettings/TileService.java
+++ b/core/java/android/service/quicksettings/TileService.java
@@ -15,6 +15,7 @@
  */
 package android.service.quicksettings;
 
+import android.annotation.NonNull;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.SystemApi;
@@ -41,6 +42,8 @@
 
 import com.android.internal.R;
 
+import java.util.Objects;
+
 /**
  * A TileService provides the user a tile that can be added to Quick Settings.
  * Quick Settings is a space provided that allows the user to change settings and
@@ -341,9 +344,9 @@
      * Will collapse Quick Settings after launching.
      *
      * @param pendingIntent A PendingIntent for an Activity to be launched immediately.
-     * @hide
      */
-    public void startActivityAndCollapse(PendingIntent pendingIntent) {
+    public final void startActivityAndCollapse(@NonNull PendingIntent pendingIntent) {
+        Objects.requireNonNull(pendingIntent);
         try {
             mService.startActivity(mTileToken, pendingIntent);
         } catch (RemoteException e) {
diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractDetector.java
similarity index 89%
rename from core/java/android/service/voice/AbstractHotwordDetector.java
rename to core/java/android/service/voice/AbstractDetector.java
index c90ab67..db0ede5 100644
--- a/core/java/android/service/voice/AbstractHotwordDetector.java
+++ b/core/java/android/service/voice/AbstractDetector.java
@@ -41,9 +41,17 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 
-/** Base implementation of {@link HotwordDetector}. */
-abstract class AbstractHotwordDetector implements HotwordDetector {
-    private static final String TAG = AbstractHotwordDetector.class.getSimpleName();
+/** Base implementation of {@link HotwordDetector}.
+ *
+ * This class provides methods to manage the detector lifecycle for both
+ * {@link HotwordDetectionService} and {@link VisualQueryDetectionService}. We keep the name of the
+ * interface {@link HotwordDetector} since {@link VisualQueryDetectionService} can be logically
+ * treated as a visual activation hotword detection and also because of the existing public
+ * interface. To avoid confusion on the naming between the trusted hotword framework and the actual
+ * isolated {@link HotwordDetectionService}, the hotword from the names is removed.
+ */
+abstract class AbstractDetector implements HotwordDetector {
+    private static final String TAG = AbstractDetector.class.getSimpleName();
     private static final boolean DEBUG = false;
 
     protected final Object mLock = new Object();
@@ -51,14 +59,14 @@
     private final IVoiceInteractionManagerService mManagerService;
     private final Handler mHandler;
     private final HotwordDetector.Callback mCallback;
-    private Consumer<AbstractHotwordDetector> mOnDestroyListener;
+    private Consumer<AbstractDetector> 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(
+    AbstractDetector(
             IVoiceInteractionManagerService managerService,
             HotwordDetector.Callback callback) {
         mManagerService = managerService;
@@ -139,7 +147,7 @@
         }
     }
 
-    void registerOnDestroyListener(Consumer<AbstractHotwordDetector> onDestroyListener) {
+    void registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener) {
         synchronized (mLock) {
             if (mOnDestroyListener != null) {
                 throw new IllegalStateException("only one destroy listener can be registered");
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 9008bf7..e8e8f1a 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -81,7 +81,7 @@
  *                    mark and track it as such.
  */
 @SystemApi
-public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
+public class AlwaysOnHotwordDetector extends AbstractDetector {
     //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----//
     /**
      * Indicates that this hotword detector is no longer valid for any recognition
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index f1b7745..ffc6621 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -46,7 +46,7 @@
  *
  * @hide
  **/
-class SoftwareHotwordDetector extends AbstractHotwordDetector {
+class SoftwareHotwordDetector extends AbstractDetector {
     private static final String TAG = SoftwareHotwordDetector.class.getSimpleName();
     private static final boolean DEBUG = false;
 
diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java
new file mode 100644
index 0000000..d8266f3
--- /dev/null
+++ b/core/java/android/service/voice/VisualQueryDetectionService.java
@@ -0,0 +1,198 @@
+/*
+ * 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.service.voice;
+
+import android.annotation.DurationMillisLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.ContentCaptureOptions;
+import android.content.Intent;
+import android.hardware.soundtrigger.SoundTrigger;
+import android.media.AudioFormat;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.SharedMemory;
+import android.speech.IRecognitionServiceManager;
+import android.util.Log;
+import android.view.contentcapture.IContentCaptureManager;
+
+import java.util.function.IntConsumer;
+
+/**
+ * Implemented by an application that wants to offer query detection with visual signals.
+ *
+ * This service leverages visual signals such as camera frames to detect and stream queries from the
+ * device microphone to the {@link VoiceInteractionService}, without the support of hotword. The
+ * system will bind an application's {@link VoiceInteractionService} first. When
+ * {@link VoiceInteractionService#createVisualQueryDetector(PersistableBundle, SharedMemory,
+ * Executor, VisualQueryDetector.Callback)} is called, the system will bind the application's
+ * {@link VisualQueryDetectionService}. When requested from {@link VoiceInteractionService}, the
+ * system calls into the {@link VisualQueryDetectionService#onStartDetection(Callback)} to enable
+ * detection. This method MUST be implemented to support visual query detection service.
+ *
+ * Note: Methods in this class may be called concurrently.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class VisualQueryDetectionService extends Service
+        implements SandboxedDetectionServiceBase {
+
+    private static final String TAG = VisualQueryDetectionService.class.getSimpleName();
+
+    private static final long UPDATE_TIMEOUT_MILLIS = 20000;
+
+    /**
+     * The {@link Intent} that must be declared as handled by the service.
+     * To be supported, the service must also require the
+     * {@link android.Manifest.permission#BIND_VISUAL_QUERY_DETECTION_SERVICE} permission
+     * so that other applications can not abuse it.
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+    public static final String SERVICE_INTERFACE =
+            "android.service.voice.VisualQueryDetectionService";
+
+
+    /** @hide */
+    public static final String KEY_INITIALIZATION_STATUS = "initialization_status";
+
+
+    private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.Stub() {
+
+        @Override
+        public void updateState(PersistableBundle options, SharedMemory sharedMemory,
+                IRemoteCallback callback) throws RemoteException {
+            Log.v(TAG, "#updateState" + (callback != null ? " with callback" : ""));
+            VisualQueryDetectionService.this.onUpdateStateInternal(
+                    options,
+                    sharedMemory,
+                    callback);
+        }
+
+        @Override
+        public void ping(IRemoteCallback callback) throws RemoteException {
+            callback.sendResult(null);
+        }
+
+        @Override
+        public void detectFromDspSource(
+                SoundTrigger.KeyphraseRecognitionEvent event,
+                AudioFormat audioFormat,
+                long timeoutMillis,
+                IDspHotwordDetectionCallback callback) {
+            throw new UnsupportedOperationException("Not supported by VisualQueryDetectionService");
+        }
+
+        @Override
+        public void detectFromMicrophoneSource(
+                ParcelFileDescriptor audioStream,
+                @HotwordDetectionService.AudioSource int audioSource,
+                AudioFormat audioFormat,
+                PersistableBundle options,
+                IDspHotwordDetectionCallback callback) {
+            throw new UnsupportedOperationException("Not supported by VisualQueryDetectionService");
+        }
+
+        @Override
+        public void updateAudioFlinger(IBinder audioFlinger) {
+            Log.v(TAG, "Ignore #updateAudioFlinger");
+        }
+
+        @Override
+        public void updateContentCaptureManager(IContentCaptureManager manager,
+                ContentCaptureOptions options) {
+            Log.v(TAG, "Ignore #updateContentCaptureManager");
+        }
+
+        @Override
+        public void updateRecognitionServiceManager(IRecognitionServiceManager manager) {
+            Log.v(TAG, "Ignore #updateRecognitionServiceManager");
+        }
+
+        @Override
+        public void stopDetection() {
+            throw new UnsupportedOperationException("Not supported by VisualQueryDetectionService");
+        }
+    };
+
+    /**
+     * {@inheritDoc}
+     * @hide
+     */
+    @Override
+    @SystemApi
+    public void onUpdateState(
+            @Nullable PersistableBundle options,
+            @Nullable SharedMemory sharedMemory,
+            @DurationMillisLong long callbackTimeoutMillis,
+            @Nullable IntConsumer statusCallback) {
+    }
+
+    @Override
+    @Nullable
+    public IBinder onBind(@NonNull Intent intent) {
+        if (SERVICE_INTERFACE.equals(intent.getAction())) {
+            return mInterface.asBinder();
+        }
+        Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": "
+                + intent);
+        return null;
+    }
+
+    private void onUpdateStateInternal(@Nullable PersistableBundle options,
+            @Nullable SharedMemory sharedMemory, IRemoteCallback callback) {
+        IntConsumer intConsumer =
+                SandboxedDetectionServiceBase.createInitializationStatusConsumer(callback);
+        onUpdateState(options, sharedMemory, UPDATE_TIMEOUT_MILLIS, intConsumer);
+    }
+
+    /**
+     * This is called after the service is set up and the client should open the camera and the
+     * microphone to start recognition.
+     *
+     * Called when the {@link VoiceInteractionService} requests that this service
+     * {@link HotwordDetector#startRecognition()} start recognition on audio coming directly
+     * from the device microphone.
+     *
+     * @param callback The callback to use for responding to the detection request.
+     *
+     */
+    public void onStartDetection(@NonNull Callback callback) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Called when the {@link VoiceInteractionService}
+     * {@link HotwordDetector#stopRecognition()} requests that recognition be stopped.
+     */
+    public void onStopDetection() {
+    }
+
+    /**
+     * Callback for sending out signals and returning query results.
+     */
+    public static final class Callback {
+        //TODO: Add callback to send signals to VIS and SysUI.
+    }
+
+}
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index a59578e..9e1518d 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -168,7 +168,7 @@
 
     private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
 
-    private final Set<HotwordDetector> mActiveHotwordDetectors = new ArraySet<>();
+    private final Set<HotwordDetector> mActiveDetectors = new ArraySet<>();
 
     /**
      * Called when a user has activated an affordance to launch voice assist from the Keyguard.
@@ -319,7 +319,7 @@
     private void onSoundModelsChangedInternal() {
         synchronized (this) {
             // TODO: Stop recognition if a sound model that was being recognized gets deleted.
-            mActiveHotwordDetectors.forEach(detector -> {
+            mActiveDetectors.forEach(detector -> {
                 if (detector instanceof AlwaysOnHotwordDetector) {
                     ((AlwaysOnHotwordDetector) detector).onSoundModelsChanged();
                 }
@@ -429,7 +429,7 @@
                 // Allow only one concurrent recognition via the APIs.
                 safelyShutdownAllHotwordDetectors();
             } else {
-                for (HotwordDetector detector : mActiveHotwordDetectors) {
+                for (HotwordDetector detector : mActiveDetectors) {
                     if (detector.isUsingSandboxedDetectionService()
                             != supportHotwordDetectionService) {
                         throw new IllegalStateException(
@@ -447,13 +447,13 @@
                     callback, mKeyphraseEnrollmentInfo, mSystemService,
                     getApplicationContext().getApplicationInfo().targetSdkVersion,
                     supportHotwordDetectionService);
-            mActiveHotwordDetectors.add(dspDetector);
+            mActiveDetectors.add(dspDetector);
 
             try {
                 dspDetector.registerOnDestroyListener(this::onHotwordDetectorDestroyed);
                 dspDetector.initialize(options, sharedMemory);
             } catch (Exception e) {
-                mActiveHotwordDetectors.remove(dspDetector);
+                mActiveDetectors.remove(dspDetector);
                 dspDetector.destroy();
                 throw e;
             }
@@ -512,7 +512,7 @@
                 // Allow only one concurrent recognition via the APIs.
                 safelyShutdownAllHotwordDetectors();
             } else {
-                for (HotwordDetector detector : mActiveHotwordDetectors) {
+                for (HotwordDetector detector : mActiveDetectors) {
                     if (!detector.isUsingSandboxedDetectionService()) {
                         throw new IllegalStateException(
                                 "It disallows to create trusted and non-trusted detectors "
@@ -528,14 +528,14 @@
             SoftwareHotwordDetector softwareHotwordDetector =
                     new SoftwareHotwordDetector(
                             mSystemService, null, callback);
-            mActiveHotwordDetectors.add(softwareHotwordDetector);
+            mActiveDetectors.add(softwareHotwordDetector);
 
             try {
                 softwareHotwordDetector.registerOnDestroyListener(
                         this::onHotwordDetectorDestroyed);
                 softwareHotwordDetector.initialize(options, sharedMemory);
             } catch (Exception e) {
-                mActiveHotwordDetectors.remove(softwareHotwordDetector);
+                mActiveDetectors.remove(softwareHotwordDetector);
                 softwareHotwordDetector.destroy();
                 throw e;
             }
@@ -586,7 +586,7 @@
 
     private void safelyShutdownAllHotwordDetectors() {
         synchronized (mLock) {
-            mActiveHotwordDetectors.forEach(detector -> {
+            mActiveDetectors.forEach(detector -> {
                 try {
                     detector.destroy();
                 } catch (Exception ex) {
@@ -598,13 +598,13 @@
 
     private void onHotwordDetectorDestroyed(@NonNull HotwordDetector detector) {
         synchronized (mLock) {
-            mActiveHotwordDetectors.remove(detector);
+            mActiveDetectors.remove(detector);
             shutdownHotwordDetectionServiceIfRequiredLocked();
         }
     }
 
     private void shutdownHotwordDetectionServiceIfRequiredLocked() {
-        for (HotwordDetector detector : mActiveHotwordDetectors) {
+        for (HotwordDetector detector : mActiveDetectors) {
             if (detector.isUsingSandboxedDetectionService()) {
                 return;
             }
@@ -638,11 +638,11 @@
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("VOICE INTERACTION");
         synchronized (mLock) {
-            pw.println("  HotwordDetector(s)");
-            if (mActiveHotwordDetectors.size() == 0) {
+            pw.println("  Sandboxed Detector(s)");
+            if (mActiveDetectors.size() == 0) {
                 pw.println("    NULL");
             } else {
-                mActiveHotwordDetectors.forEach(detector -> {
+                mActiveDetectors.forEach(detector -> {
                     detector.dump("    ", pw);
                     pw.println();
                 });
diff --git a/core/java/android/view/EventLogTags.logtags b/core/java/android/view/EventLogTags.logtags
index 1ad3472..cc0b18a 100644
--- a/core/java/android/view/EventLogTags.logtags
+++ b/core/java/android/view/EventLogTags.logtags
@@ -44,6 +44,12 @@
 32007 imf_ime_anim_finish (token|3),(animation type|1),(alpha|5),(shown|1),(insets|3)
 # IME animation is canceled.
 32008 imf_ime_anim_cancel (token|3),(animation type|1),(pending insets|3)
+# IME remote animation is started.
+32009 imf_ime_remote_anim_start (token|3),(displayId|1),(direction|1),(alpha|5),(startY|5),(endY|5),(leash|3),(insets|3),(surface position|3),(ime frame|3)
+# IME remote animation is end.
+32010 imf_ime_remote_anim_end (token|3),(displayId|1),(direction|1),(endY|5),(leash|3),(insets|3),(surface position|3),(ime frame|3)
+# IME remote animation is canceled.
+32011 imf_ime_remote_anim_cancel (token|3),(displayId|1),(insets|3)
 
 # 62000 - 62199 reserved for inputflinger
 
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 0aba80d..e38e537 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -721,8 +721,7 @@
      *
      * @return {@code true} if system bars are always consumed.
      */
-    boolean getWindowInsets(in WindowManager.LayoutParams attrs, int displayId,
-            out InsetsState outInsetsState);
+    boolean getWindowInsets(int displayId, in IBinder token, out InsetsState outInsetsState);
 
     /**
      * Returns a list of {@link android.view.DisplayInfo} for the logical display. This is not
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index f375ccb..43cf758 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -814,6 +814,45 @@
     }
 
     /**
+     * Activity level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the activity can be opted-in or opted-out
+     * from the compatibility treatment that avoids {@link
+     * android.app.Activity#setRequestedOrientation} loops. The loop can be trigerred by
+     * ignoreRequestedOrientation display setting enabled on the device or by the landscape natural
+     * orientation of the device.
+     *
+     * <p>The treatment is disabled by default but device manufacturers can enable the treatment
+     * using their discretion to improve display compatibility.
+     *
+     * <p>With this property set to {@code true}, the system could ignore {@link
+     * android.app.Activity#setRequestedOrientation} call from an app if one of the following
+     * conditions are true:
+     * <ul>
+     *     <li>Activity is relaunching due to the previous {@link
+     *     android.app.Activity#setRequestedOrientation} call.
+     *     <li>Camera compatibility force rotation treatment is active for the package.
+     * </ul>
+     *
+     * <p>Setting this property to {@code false} informs the system that the activity must be
+     * opted-out from the compatibility treatment even if the device manufacturer has opted the app
+     * into the treatment.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;activity&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"
+     *     android:value="true|false"/&gt;
+     * &lt;/activity&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION =
+            "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
+
+    /**
      * @hide
      */
     public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array";
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index 9ed5c29..3b6ec80 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -44,8 +44,7 @@
     String TAG = "ImeTracker";
 
     /** The debug flag for IME visibility event log. */
-    // TODO(b/239501597) : Have a system property to control this flag.
-    boolean DEBUG_IME_VISIBILITY = false;
+    boolean DEBUG_IME_VISIBILITY = SystemProperties.getBoolean("persist.debug.imf_event", false);
 
     /** The message to indicate if there is no valid {@link Token}. */
     String TOKEN_NONE = "TOKEN_NONE";
diff --git a/core/java/android/window/TaskFragmentAnimationParams.aidl b/core/java/android/window/TaskFragmentAnimationParams.aidl
new file mode 100644
index 0000000..04dee58
--- /dev/null
+++ b/core/java/android/window/TaskFragmentAnimationParams.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.window;
+
+/**
+ * Data object for animation related override of TaskFragment.
+ * @hide
+ */
+parcelable TaskFragmentAnimationParams;
diff --git a/core/java/android/window/TaskFragmentAnimationParams.java b/core/java/android/window/TaskFragmentAnimationParams.java
new file mode 100644
index 0000000..a600a4d
--- /dev/null
+++ b/core/java/android/window/TaskFragmentAnimationParams.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data object for animation related override of TaskFragment.
+ * @hide
+ */
+// TODO(b/206557124): Add more animation customization options.
+public final class TaskFragmentAnimationParams implements Parcelable {
+
+    /** The default {@link TaskFragmentAnimationParams} to use when there is no app override. */
+    public static final TaskFragmentAnimationParams DEFAULT =
+            new TaskFragmentAnimationParams.Builder().build();
+
+    @ColorInt
+    private final int mAnimationBackgroundColor;
+
+    private TaskFragmentAnimationParams(@ColorInt int animationBackgroundColor) {
+        mAnimationBackgroundColor = animationBackgroundColor;
+    }
+
+    /**
+     * The {@link ColorInt} to use for the background during the animation with this TaskFragment if
+     * the animation requires a background.
+     *
+     * The default value is {@code 0}, which is to use the theme window background.
+     */
+    @ColorInt
+    public int getAnimationBackgroundColor() {
+        return mAnimationBackgroundColor;
+    }
+
+    private TaskFragmentAnimationParams(Parcel in) {
+        mAnimationBackgroundColor = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mAnimationBackgroundColor);
+    }
+
+    @NonNull
+    public static final Creator<TaskFragmentAnimationParams> CREATOR =
+            new Creator<TaskFragmentAnimationParams>() {
+                @Override
+                public TaskFragmentAnimationParams createFromParcel(Parcel in) {
+                    return new TaskFragmentAnimationParams(in);
+                }
+
+                @Override
+                public TaskFragmentAnimationParams[] newArray(int size) {
+                    return new TaskFragmentAnimationParams[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return "TaskFragmentAnimationParams{"
+                + " animationBgColor=" + Integer.toHexString(mAnimationBackgroundColor)
+                + "}";
+    }
+
+    @Override
+    public int hashCode() {
+        return mAnimationBackgroundColor;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof TaskFragmentAnimationParams)) {
+            return false;
+        }
+        final TaskFragmentAnimationParams other = (TaskFragmentAnimationParams) obj;
+        return mAnimationBackgroundColor == other.mAnimationBackgroundColor;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Builder to construct the {@link TaskFragmentAnimationParams}. */
+    public static final class Builder {
+
+        @ColorInt
+        private int mAnimationBackgroundColor = 0;
+
+        /**
+         * Sets the {@link ColorInt} to use for the background during the animation with this
+         * TaskFragment if the animation requires a background. The default value is
+         * {@code 0}, which is to use the theme window background.
+         *
+         * @param color a packed color int, {@code AARRGGBB}, for the animation background color.
+         * @return this {@link Builder}.
+         */
+        @NonNull
+        public Builder setAnimationBackgroundColor(@ColorInt int color) {
+            mAnimationBackgroundColor = color;
+            return this;
+        }
+
+        /** Constructs the {@link TaskFragmentAnimationParams}. */
+        @NonNull
+        public TaskFragmentAnimationParams build() {
+            return new TaskFragmentAnimationParams(mAnimationBackgroundColor);
+        }
+    }
+}
diff --git a/core/java/android/window/TaskFragmentOperation.aidl b/core/java/android/window/TaskFragmentOperation.aidl
new file mode 100644
index 0000000..c21700c
--- /dev/null
+++ b/core/java/android/window/TaskFragmentOperation.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.window;
+
+/**
+ * Data object of params for TaskFragment related {@link WindowContainerTransaction} operation.
+ * @hide
+ */
+parcelable TaskFragmentOperation;
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
new file mode 100644
index 0000000..bec6c58
--- /dev/null
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Data object of params for TaskFragment related {@link WindowContainerTransaction} operation.
+ *
+ * @see WindowContainerTransaction#setTaskFragmentOperation(IBinder, TaskFragmentOperation).
+ * @hide
+ */
+// TODO(b/263436063): move other TaskFragment related operation here.
+public final class TaskFragmentOperation implements Parcelable {
+
+    /** Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment. */
+    public static final int OP_TYPE_SET_ANIMATION_PARAMS = 0;
+
+    @IntDef(prefix = { "OP_TYPE_" }, value = {
+            OP_TYPE_SET_ANIMATION_PARAMS
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface OperationType {}
+
+    @OperationType
+    private final int mOpType;
+
+    @Nullable
+    private final TaskFragmentAnimationParams mAnimationParams;
+
+    private TaskFragmentOperation(@OperationType int opType,
+            @Nullable TaskFragmentAnimationParams animationParams) {
+        mOpType = opType;
+        mAnimationParams = animationParams;
+    }
+
+    private TaskFragmentOperation(Parcel in) {
+        mOpType = in.readInt();
+        mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mOpType);
+        dest.writeTypedObject(mAnimationParams, flags);
+    }
+
+    @NonNull
+    public static final Creator<TaskFragmentOperation> CREATOR =
+            new Creator<TaskFragmentOperation>() {
+                @Override
+                public TaskFragmentOperation createFromParcel(Parcel in) {
+                    return new TaskFragmentOperation(in);
+                }
+
+                @Override
+                public TaskFragmentOperation[] newArray(int size) {
+                    return new TaskFragmentOperation[size];
+                }
+            };
+
+    /**
+     * Gets the {@link OperationType} of this {@link TaskFragmentOperation}.
+     */
+    @OperationType
+    public int getOpType() {
+        return mOpType;
+    }
+
+    /**
+     * Gets the animation related override of TaskFragment.
+     */
+    @Nullable
+    public TaskFragmentAnimationParams getAnimationParams() {
+        return mAnimationParams;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("TaskFragmentOperation{ opType=").append(mOpType);
+        if (mAnimationParams != null) {
+            sb.append(", animationParams=").append(mAnimationParams);
+        }
+
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mOpType;
+        result = result * 31 + mAnimationParams.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof TaskFragmentOperation)) {
+            return false;
+        }
+        final TaskFragmentOperation other = (TaskFragmentOperation) obj;
+        return mOpType == other.mOpType
+                && Objects.equals(mAnimationParams, other.mAnimationParams);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Builder to construct the {@link TaskFragmentOperation}. */
+    public static final class Builder {
+
+        @OperationType
+        private final int mOpType;
+
+        @Nullable
+        private TaskFragmentAnimationParams mAnimationParams;
+
+        /**
+         * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}.
+         */
+        public Builder(@OperationType int opType) {
+            mOpType = opType;
+        }
+
+        /**
+         * Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment.
+         */
+        @NonNull
+        public Builder setAnimationParams(@Nullable TaskFragmentAnimationParams animationParams) {
+            mAnimationParams = animationParams;
+            return this;
+        }
+
+        /**
+         * Constructs the {@link TaskFragmentOperation}.
+         */
+        @NonNull
+        public TaskFragmentOperation build() {
+            return new TaskFragmentOperation(mOpType, mAnimationParams);
+        }
+    }
+}
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 5793674..647ccf5 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -751,6 +751,30 @@
     }
 
     /**
+     * Sets the {@link TaskFragmentOperation} to apply to the given TaskFragment.
+     *
+     * @param fragmentToken client assigned unique token to create TaskFragment with specified in
+     *                      {@link TaskFragmentCreationParams#getFragmentToken()}.
+     * @param taskFragmentOperation the {@link TaskFragmentOperation} to apply to the given
+     *                              TaskFramgent.
+     * @hide
+     */
+    @NonNull
+    public WindowContainerTransaction setTaskFragmentOperation(@NonNull IBinder fragmentToken,
+            @NonNull TaskFragmentOperation taskFragmentOperation) {
+        Objects.requireNonNull(fragmentToken);
+        Objects.requireNonNull(taskFragmentOperation);
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(
+                        HierarchyOp.HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION)
+                        .setContainer(fragmentToken)
+                        .setTaskFragmentOperation(taskFragmentOperation)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
      * Sets/removes the always on top flag for this {@code windowContainer}. See
      * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}.
      * Please note that this method is only intended to be used for a
@@ -1261,6 +1285,7 @@
         public static final int HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 22;
         public static final int HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS = 23;
         public static final int HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH = 24;
+        public static final int HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION = 25;
 
         // The following key(s) are for use with mLaunchOptions:
         // When launching a task (eg. from recents), this is the taskId to be launched.
@@ -1301,10 +1326,14 @@
         @Nullable
         private Intent mActivityIntent;
 
-        // Used as options for WindowContainerTransaction#createTaskFragment().
+        /** Used as options for {@link #createTaskFragment}. */
         @Nullable
         private TaskFragmentCreationParams mTaskFragmentCreationOptions;
 
+        /** Used as options for {@link #setTaskFragmentOperation}. */
+        @Nullable
+        private TaskFragmentOperation mTaskFragmentOperation;
+
         @Nullable
         private PendingIntent mPendingIntent;
 
@@ -1424,6 +1453,7 @@
             mLaunchOptions = copy.mLaunchOptions;
             mActivityIntent = copy.mActivityIntent;
             mTaskFragmentCreationOptions = copy.mTaskFragmentCreationOptions;
+            mTaskFragmentOperation = copy.mTaskFragmentOperation;
             mPendingIntent = copy.mPendingIntent;
             mShortcutInfo = copy.mShortcutInfo;
             mAlwaysOnTop = copy.mAlwaysOnTop;
@@ -1447,6 +1477,7 @@
             mLaunchOptions = in.readBundle();
             mActivityIntent = in.readTypedObject(Intent.CREATOR);
             mTaskFragmentCreationOptions = in.readTypedObject(TaskFragmentCreationParams.CREATOR);
+            mTaskFragmentOperation = in.readTypedObject(TaskFragmentOperation.CREATOR);
             mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
             mShortcutInfo = in.readTypedObject(ShortcutInfo.CREATOR);
             mAlwaysOnTop = in.readBoolean();
@@ -1535,6 +1566,11 @@
         }
 
         @Nullable
+        public TaskFragmentOperation getTaskFragmentOperation() {
+            return mTaskFragmentOperation;
+        }
+
+        @Nullable
         public PendingIntent getPendingIntent() {
             return mPendingIntent;
         }
@@ -1612,6 +1648,9 @@
                 case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH:
                     return "{setReparentLeafTaskIfRelaunch: container= " + mContainer
                             + " reparentLeafTaskIfRelaunch= " + mReparentLeafTaskIfRelaunch + "}";
+                case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION:
+                    return "{setTaskFragmentOperation: fragmentToken= " + mContainer
+                            + " operation= " + mTaskFragmentOperation + "}";
                 default:
                     return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent
                             + " mToTop=" + mToTop
@@ -1639,6 +1678,7 @@
             dest.writeBundle(mLaunchOptions);
             dest.writeTypedObject(mActivityIntent, flags);
             dest.writeTypedObject(mTaskFragmentCreationOptions, flags);
+            dest.writeTypedObject(mTaskFragmentOperation, flags);
             dest.writeTypedObject(mPendingIntent, flags);
             dest.writeTypedObject(mShortcutInfo, flags);
             dest.writeBoolean(mAlwaysOnTop);
@@ -1696,6 +1736,9 @@
             private TaskFragmentCreationParams mTaskFragmentCreationOptions;
 
             @Nullable
+            private TaskFragmentOperation mTaskFragmentOperation;
+
+            @Nullable
             private PendingIntent mPendingIntent;
 
             @Nullable
@@ -1775,6 +1818,12 @@
                 return this;
             }
 
+            Builder setTaskFragmentOperation(
+                    @Nullable TaskFragmentOperation taskFragmentOperation) {
+                mTaskFragmentOperation = taskFragmentOperation;
+                return this;
+            }
+
             Builder setReparentLeafTaskIfRelaunch(boolean reparentLeafTaskIfRelaunch) {
                 mReparentLeafTaskIfRelaunch = reparentLeafTaskIfRelaunch;
                 return this;
@@ -1804,6 +1853,7 @@
                 hierarchyOp.mPendingIntent = mPendingIntent;
                 hierarchyOp.mAlwaysOnTop = mAlwaysOnTop;
                 hierarchyOp.mTaskFragmentCreationOptions = mTaskFragmentCreationOptions;
+                hierarchyOp.mTaskFragmentOperation = mTaskFragmentOperation;
                 hierarchyOp.mShortcutInfo = mShortcutInfo;
                 hierarchyOp.mReparentLeafTaskIfRelaunch = mReparentLeafTaskIfRelaunch;
 
diff --git a/core/java/android/window/WindowMetricsController.java b/core/java/android/window/WindowMetricsController.java
index 5b08879..06449d5 100644
--- a/core/java/android/window/WindowMetricsController.java
+++ b/core/java/android/window/WindowMetricsController.java
@@ -24,8 +24,10 @@
 import android.app.ResourcesManager;
 import android.app.WindowConfiguration;
 import android.content.Context;
+import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.DisplayMetrics;
 import android.view.Display;
@@ -89,23 +91,16 @@
             isScreenRound = config.isScreenRound();
             windowingMode = winConfig.getWindowingMode();
         }
-        final WindowInsets windowInsets = computeWindowInsets(bounds, isScreenRound, windowingMode);
+        final IBinder token = Context.getToken(mContext);
+        final WindowInsets windowInsets = getWindowInsetsFromServerForCurrentDisplay(token,
+                bounds, isScreenRound, windowingMode);
         return new WindowMetrics(bounds, windowInsets, density);
     }
 
-    private WindowInsets computeWindowInsets(Rect bounds, boolean isScreenRound,
-            @WindowConfiguration.WindowingMode int windowingMode) {
-        // Initialize params which used for obtaining all system insets.
-        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
-        params.token = Context.getToken(mContext);
-        return getWindowInsetsFromServerForCurrentDisplay(params, bounds, isScreenRound,
-                windowingMode);
-    }
-
     private WindowInsets getWindowInsetsFromServerForCurrentDisplay(
-            WindowManager.LayoutParams attrs, Rect bounds, boolean isScreenRound,
+            IBinder token, Rect bounds, boolean isScreenRound,
             @WindowConfiguration.WindowingMode int windowingMode) {
-        return getWindowInsetsFromServerForDisplay(mContext.getDisplayId(), attrs, bounds,
+        return getWindowInsetsFromServerForDisplay(mContext.getDisplayId(), token, bounds,
                 isScreenRound, windowingMode);
     }
 
@@ -113,22 +108,26 @@
      * Retrieves WindowInsets for the given context and display, given the window bounds.
      *
      * @param displayId the ID of the logical display to calculate insets for
-     * @param attrs the LayoutParams for the calling app
+     * @param token the token of Activity or WindowContext
      * @param bounds the window bounds to calculate insets for
      * @param isScreenRound if the display identified by displayId is round
      * @param windowingMode the windowing mode of the window to calculate insets for
      * @return WindowInsets calculated for the given window bounds, on the given display
      */
-    private static WindowInsets getWindowInsetsFromServerForDisplay(int displayId,
-            WindowManager.LayoutParams attrs, Rect bounds, boolean isScreenRound,
-            int windowingMode) {
+    private static WindowInsets getWindowInsetsFromServerForDisplay(int displayId, IBinder token,
+            Rect bounds, boolean isScreenRound, int windowingMode) {
         try {
             final InsetsState insetsState = new InsetsState();
             final boolean alwaysConsumeSystemBars = WindowManagerGlobal.getWindowManagerService()
-                    .getWindowInsets(attrs, displayId, insetsState);
-            return insetsState.calculateInsets(bounds, null /* ignoringVisibilityState*/,
-                    isScreenRound, alwaysConsumeSystemBars, SOFT_INPUT_ADJUST_NOTHING, attrs.flags,
-                    SYSTEM_UI_FLAG_VISIBLE, attrs.type, windowingMode,
+                    .getWindowInsets(displayId, token, insetsState);
+            final float overrideInvScale = CompatibilityInfo.getOverrideInvertedScale();
+            if (overrideInvScale != 1f) {
+                insetsState.scale(overrideInvScale);
+            }
+            return insetsState.calculateInsets(bounds, null /* ignoringVisibilityState */,
+                    isScreenRound, alwaysConsumeSystemBars, SOFT_INPUT_ADJUST_NOTHING,
+                    0 /* flags */, SYSTEM_UI_FLAG_VISIBLE,
+                    WindowManager.LayoutParams.INVALID_WINDOW_TYPE, windowingMode,
                     null /* typeSideMap */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -149,7 +148,6 @@
         Set<WindowMetrics> maxMetrics = new HashSet<>();
         WindowInsets windowInsets;
         DisplayInfo currentDisplayInfo;
-        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
         for (int i = 0; i < possibleDisplayInfos.size(); i++) {
             currentDisplayInfo = possibleDisplayInfos.get(i);
 
@@ -162,7 +160,7 @@
             // Initialize insets based upon display rotation. Note any window-provided insets
             // will not be set.
             windowInsets = getWindowInsetsFromServerForDisplay(
-                    currentDisplayInfo.displayId, params,
+                    currentDisplayInfo.displayId, null /* token */,
                     new Rect(0, 0, currentDisplayInfo.getNaturalWidth(),
                             currentDisplayInfo.getNaturalHeight()), isScreenRound,
                     WINDOWING_MODE_FULLSCREEN);
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index e031e5f..5f1a444 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3121,6 +3121,34 @@
     <permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS"
                 android:protectionLevel="signature|role" />
 
+    <!-- Allows an application to manage date and time device policy. -->
+    <permission android:name="android.permission.MANAGE_DEVICE_POLICY_TIME"
+                android:protectionLevel="internal|role" />
+
+    <!-- Allows an application to set device policies outside the current user
+        that are critical for securing data within the current user.
+        <p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_*
+            permissions across all users on the device provided they are required for securing data
+            within the current user.-->
+    <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL"
+                android:protectionLevel="internal|role" />
+
+    <!-- Allows an application to set device policies outside the current user
+        that are required for securing device ownership without accessing user data.
+        <p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_*
+            permissions across all users on the device provided they do not grant access to user
+            data. -->
+    <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS"
+                android:protectionLevel="internal|role" />
+
+    <!-- Allows an application to set device policies outside the current user.
+        <p>Fuller form of {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_ACROSS_USERS}
+             that removes the restriction on accessing user data.
+        <p>Holding this permission allows the use of any other held MANAGE_DEVICE_POLICY_*
+            permissions across all users on the device.-->
+    <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL"
+                android:protectionLevel="internal|role" />
+
     <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
     <permission android:name="android.permission.PROVISION_DEMO_DEVICE"
                 android:protectionLevel="signature|setup" />
@@ -5977,7 +6005,7 @@
     <permission android:name="android.permission.START_VIEW_PERMISSION_USAGE"
         android:label="@string/permlab_startViewPermissionUsage"
         android:description="@string/permdesc_startViewPermissionUsage"
-        android:protectionLevel="signature|installer" />
+        android:protectionLevel="signature|installer|module" />
 
     <!--
         @SystemApi
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2310367..aad88c4 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5360,6 +5360,11 @@
         If given value is outside of this range, the option 0 (top) is assummed. -->
     <integer name="config_letterboxDefaultPositionForTabletopModeReachability">0</integer>
 
+    <!-- Whether should ignore app requested orientation in response to an app
+         calling Activity#setRequestedOrientation. See
+         LetterboxUiController#shouldIgnoreRequestedOrientation for details. -->
+    <bool name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled">false</bool>
+
     <!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. -->
     <bool name="config_letterboxIsEducationEnabled">false</bool>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index af29b23..85bafb9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -945,7 +945,6 @@
   <java-symbol type="string" name="serviceErased" />
   <java-symbol type="string" name="serviceNotProvisioned" />
   <java-symbol type="string" name="serviceRegistered" />
-  <java-symbol type="string" name="setup_autofill" />
   <java-symbol type="string" name="share" />
   <java-symbol type="string" name="shareactionprovider_share_with" />
   <java-symbol type="string" name="shareactionprovider_share_with_application" />
@@ -4430,6 +4429,7 @@
   <java-symbol type="integer" name="config_letterboxDefaultPositionForVerticalReachability" />
   <java-symbol type="integer" name="config_letterboxDefaultPositionForBookModeReachability" />
   <java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" />
+  <java-symbol type="bool" name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled" />
   <java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
   <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
   <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
diff --git a/core/tests/coretests/src/android/companion/virtual/OWNERS b/core/tests/coretests/src/android/companion/virtual/OWNERS
index 1a3e927..2e475a9 100644
--- a/core/tests/coretests/src/android/companion/virtual/OWNERS
+++ b/core/tests/coretests/src/android/companion/virtual/OWNERS
@@ -1,3 +1 @@
-set noparent
-
 include /services/companion/java/com/android/server/companion/virtual/OWNERS
\ No newline at end of file
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index b910287..87fa63d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -17,6 +17,7 @@
 package androidx.window.extensions.embedding;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
 
 import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
 import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
@@ -31,8 +32,10 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.util.ArrayMap;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentCreationParams;
 import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOperation;
 import android.window.TaskFragmentOrganizer;
 import android.window.TaskFragmentTransaction;
 import android.window.WindowContainerTransaction;
@@ -114,13 +117,14 @@
      * @param activityIntent    Intent to start the secondary Activity with.
      * @param activityOptions   ActivityOptions to start the secondary Activity with.
      * @param windowingMode     the windowing mode to set for the TaskFragments.
+     * @param splitAttributes   the {@link SplitAttributes} to represent the split.
      */
     void startActivityToSide(@NonNull WindowContainerTransaction wct,
             @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingFragmentBounds,
             @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken,
             @NonNull Rect secondaryFragmentBounds, @NonNull Intent activityIntent,
             @Nullable Bundle activityOptions, @NonNull SplitRule rule,
-            @WindowingMode int windowingMode) {
+            @WindowingMode int windowingMode, @NonNull SplitAttributes splitAttributes) {
         final IBinder ownerToken = launchingActivity.getActivityToken();
 
         // Create or resize the launching TaskFragment.
@@ -131,6 +135,7 @@
             createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken,
                     launchingFragmentBounds, windowingMode, launchingActivity);
         }
+        updateAnimationParams(wct, launchingFragmentToken, splitAttributes);
 
         // Create a TaskFragment for the secondary activity.
         final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder(
@@ -144,6 +149,7 @@
                 .setPairedPrimaryFragmentToken(launchingFragmentToken)
                 .build();
         createTaskFragment(wct, fragmentOptions);
+        updateAnimationParams(wct, secondaryFragmentToken, splitAttributes);
         wct.startActivityInTaskFragment(secondaryFragmentToken, ownerToken, activityIntent,
                 activityOptions);
 
@@ -163,6 +169,7 @@
         resizeTaskFragment(wct, fragmentToken, new Rect());
         setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
         updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
+        updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
     }
 
     /**
@@ -175,6 +182,7 @@
         createTaskFragmentAndReparentActivity(
                 wct, fragmentToken, activity.getActivityToken(), new Rect(),
                 WINDOWING_MODE_UNDEFINED, activity);
+        updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
     }
 
     /**
@@ -270,6 +278,24 @@
         wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode);
     }
 
+    /**
+     * Updates the {@link TaskFragmentAnimationParams} for the given TaskFragment based on
+     * {@link SplitAttributes}.
+     */
+    void updateAnimationParams(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @NonNull SplitAttributes splitAttributes) {
+        updateAnimationParams(wct, fragmentToken, createAnimationParamsOrDefault(splitAttributes));
+    }
+
+    void updateAnimationParams(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) {
+        final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+                OP_TYPE_SET_ANIMATION_PARAMS)
+                .setAnimationParams(animationParams)
+                .build();
+        wct.setTaskFragmentOperation(fragmentToken, operation);
+    }
+
     void deleteTaskFragment(@NonNull WindowContainerTransaction wct,
             @NonNull IBinder fragmentToken) {
         if (!mFragmentInfos.containsKey(fragmentToken)) {
@@ -291,4 +317,14 @@
     public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
         mCallback.onTransactionReady(transaction);
     }
+
+    private static TaskFragmentAnimationParams createAnimationParamsOrDefault(
+            @Nullable SplitAttributes splitAttributes) {
+        if (splitAttributes == null) {
+            return TaskFragmentAnimationParams.DEFAULT;
+        }
+        return new TaskFragmentAnimationParams.Builder()
+                .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
+                .build();
+    }
 }
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 ce7d695..1e004a7 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -65,6 +65,7 @@
 import android.util.Size;
 import android.util.SparseArray;
 import android.view.WindowMetrics;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentInfo;
 import android.window.TaskFragmentParentInfo;
 import android.window.TaskFragmentTransaction;
@@ -1157,6 +1158,8 @@
                 taskId);
         mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(),
                 activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED);
+        mPresenter.updateAnimationParams(wct, expandedContainer.getTaskFragmentToken(),
+                TaskFragmentAnimationParams.DEFAULT);
         return expandedContainer;
     }
 
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 9db9f87..7b2af49 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -36,6 +36,7 @@
 import android.view.View;
 import android.view.WindowInsets;
 import android.view.WindowMetrics;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentCreationParams;
 import android.window.WindowContainerTransaction;
 
@@ -176,7 +177,7 @@
         final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
                 splitAttributes);
         final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
-                primaryActivity, primaryRectBounds, null);
+                primaryActivity, primaryRectBounds, splitAttributes, null /* containerToAvoid */);
 
         // Create new empty task fragment
         final int taskId = primaryContainer.getTaskId();
@@ -189,6 +190,7 @@
         createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
                 primaryActivity.getActivityToken(), secondaryRectBounds,
                 windowingMode);
+        updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
 
         // Set adjacent to each other so that the containers below will be invisible.
         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
@@ -222,7 +224,7 @@
         final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties,
                 splitAttributes);
         final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
-                primaryActivity, primaryRectBounds, null);
+                primaryActivity, primaryRectBounds, splitAttributes, null /* containerToAvoid */);
 
         final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties,
                 splitAttributes);
@@ -236,7 +238,7 @@
             containerToAvoid = curSecondaryContainer;
         }
         final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
-                secondaryActivity, secondaryRectBounds, containerToAvoid);
+                secondaryActivity, secondaryRectBounds, splitAttributes, containerToAvoid);
 
         // Set adjacent to each other so that the containers below will be invisible.
         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
@@ -253,7 +255,8 @@
      */
     private TaskFragmentContainer prepareContainerForActivity(
             @NonNull WindowContainerTransaction wct, @NonNull Activity activity,
-            @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) {
+            @NonNull Rect bounds, @NonNull SplitAttributes splitAttributes,
+            @Nullable TaskFragmentContainer containerToAvoid) {
         TaskFragmentContainer container = mController.getContainerWithActivity(activity);
         final int taskId = container != null ? container.getTaskId() : activity.getTaskId();
         if (container == null || container == containerToAvoid) {
@@ -270,6 +273,7 @@
                     .getWindowingModeForSplitTaskFragment(bounds);
             updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode);
         }
+        updateAnimationParams(wct, container.getTaskFragmentToken(), splitAttributes);
 
         return container;
     }
@@ -314,7 +318,7 @@
                 rule, splitAttributes);
         startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
                 launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds,
-                activityIntent, activityOptions, rule, windowingMode);
+                activityIntent, activityOptions, rule, windowingMode, splitAttributes);
         if (isPlaceholder) {
             // When placeholder is launched in split, we should keep the focus on the primary.
             wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
@@ -365,6 +369,8 @@
                 primaryRectBounds);
         updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode);
         updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
+        updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
+        updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
     }
 
     private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@@ -459,6 +465,24 @@
         super.updateWindowingMode(wct, fragmentToken, windowingMode);
     }
 
+    @Override
+    void updateAnimationParams(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) {
+        final TaskFragmentContainer container = mController.getContainer(fragmentToken);
+        if (container == null) {
+            throw new IllegalStateException("Setting animation params for a task fragment that is"
+                    + " not registered with controller.");
+        }
+
+        if (container.areLastRequestedAnimationParamsEqual(animationParams)) {
+            // Return early if the animation params were already requested
+            return;
+        }
+
+        container.setLastRequestAnimationParams(animationParams);
+        super.updateAnimationParams(wct, fragmentToken, animationParams);
+    }
+
     /**
      * Expands the split container if the current split bounds are smaller than the Activity or
      * Intent that is added to the container.
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 6bfdfe7..076856c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -26,6 +26,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.util.Size;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerTransaction;
 
@@ -108,6 +109,13 @@
     private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED;
 
     /**
+     * TaskFragmentAnimationParams that was requested last via
+     * {@link android.window.WindowContainerTransaction}.
+     */
+    @NonNull
+    private TaskFragmentAnimationParams mLastAnimationParams = TaskFragmentAnimationParams.DEFAULT;
+
+    /**
      * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment
      * if it is still empty after the timeout.
      */
@@ -560,6 +568,21 @@
         mLastRequestedWindowingMode = windowingModes;
     }
 
+    /**
+     * Checks if last requested {@link TaskFragmentAnimationParams} are equal to the provided value.
+     */
+    boolean areLastRequestedAnimationParamsEqual(
+            @NonNull TaskFragmentAnimationParams animationParams) {
+        return mLastAnimationParams.equals(animationParams);
+    }
+
+    /**
+     * Updates the last requested {@link TaskFragmentAnimationParams}.
+     */
+    void setLastRequestAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) {
+        mLastAnimationParams = animationParams;
+    }
+
     /** Gets the parent leaf Task id. */
     int getTaskId() {
         return mTaskContainer.getTaskId();
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
index 13a2c78..d189ae2 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
@@ -22,6 +22,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.window.extensions.embedding.SplitAttributes;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -53,4 +54,15 @@
     public void testGetActivityEmbeddingComponent() {
         assertThat(mExtensions.getActivityEmbeddingComponent()).isNotNull();
     }
+
+    @Test
+    public void testSplitAttributes_default() {
+        // Make sure the default value in the extensions aar.
+        final SplitAttributes splitAttributes = new SplitAttributes.Builder().build();
+        assertThat(splitAttributes.getLayoutDirection())
+                .isEqualTo(SplitAttributes.LayoutDirection.LOCALE);
+        assertThat(splitAttributes.getSplitType())
+                .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f));
+        assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0);
+    }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index 6dae0a1..fcd4d62 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
 
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY;
@@ -60,12 +61,15 @@
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.util.Pair;
 import android.util.Size;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOperation;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -163,7 +167,38 @@
                 WINDOWING_MODE_MULTI_WINDOW);
 
         verify(mTransaction, never()).setWindowingMode(any(), anyInt());
+    }
 
+    @Test
+    public void testUpdateAnimationParams() {
+        final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
+
+        // Verify the default.
+        assertTrue(container.areLastRequestedAnimationParamsEqual(
+                TaskFragmentAnimationParams.DEFAULT));
+
+        final int bgColor = Color.GREEN;
+        final TaskFragmentAnimationParams animationParams =
+                new TaskFragmentAnimationParams.Builder()
+                        .setAnimationBackgroundColor(bgColor)
+                        .build();
+        mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(),
+                animationParams);
+
+        final TaskFragmentOperation expectedOperation = new TaskFragmentOperation.Builder(
+                OP_TYPE_SET_ANIMATION_PARAMS)
+                .setAnimationParams(animationParams)
+                .build();
+        verify(mTransaction).setTaskFragmentOperation(container.getTaskFragmentToken(),
+                expectedOperation);
+        assertTrue(container.areLastRequestedAnimationParamsEqual(animationParams));
+
+        // No request to set the same animation params.
+        clearInvocations(mTransaction);
+        mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(),
+                animationParams);
+
+        verify(mTransaction, never()).setTaskFragmentOperation(any(), any());
     }
 
     @Test
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 4978e04..84ab448 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml b/libs/WindowManager/Shell/res/color/decor_title_color.xml
similarity index 100%
rename from libs/WindowManager/Shell/res/color/decor_caption_title_color.xml
rename to libs/WindowManager/Shell/res/color/decor_title_color.xml
diff --git a/libs/WindowManager/Shell/res/drawable/decor_caption_menu_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml
similarity index 100%
rename from libs/WindowManager/Shell/res/drawable/decor_caption_menu_background.xml
rename to libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml
diff --git a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml
similarity index 100%
rename from libs/WindowManager/Shell/res/drawable/decor_caption_title.xml
rename to libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml
diff --git a/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml
similarity index 96%
rename from libs/WindowManager/Shell/res/layout/caption_handle_menu.xml
rename to libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml
index 582a11c..8b4792a 100644
--- a/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml
@@ -20,7 +20,7 @@
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:gravity="center_horizontal"
-android:background="@drawable/decor_caption_menu_background">
+android:background="@drawable/desktop_mode_decor_menu_background">
     <Button
         style="@style/CaptionButtonStyle"
         android:id="@+id/fullscreen_button"
diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
similarity index 93%
rename from libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
rename to libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
index 51e634c..2a4cc02 100644
--- a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
@@ -16,10 +16,10 @@
   -->
 <com.android.wm.shell.windowdecor.WindowDecorLinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/caption"
+    android:id="@+id/desktop_mode_caption"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@drawable/decor_caption_title">
+    android:background="@drawable/desktop_mode_decor_title">
     <Button
         style="@style/CaptionButtonStyle"
         android:id="@+id/back_button"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 7aae633..d0aef20 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -16,6 +16,12 @@
 
 package com.android.wm.shell.common;
 
+import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_CANCEL;
+import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_END;
+import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_START;
+import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY;
+import static android.view.inputmethod.ImeTracker.TOKEN_NONE;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
@@ -26,6 +32,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.RemoteException;
+import android.util.EventLog;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.IDisplayWindowInsetsController;
@@ -47,6 +54,7 @@
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 /**
@@ -274,29 +282,30 @@
             }
 
             if (hasImeSourceControl) {
-                final Point lastSurfacePosition = mImeSourceControl != null
-                        ? mImeSourceControl.getSurfacePosition() : null;
-                final boolean positionChanged =
-                        !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition);
-                final boolean leashChanged =
-                        !haveSameLeash(mImeSourceControl, imeSourceControl);
                 if (mAnimation != null) {
+                    final Point lastSurfacePosition = hadImeSourceControl
+                            ? mImeSourceControl.getSurfacePosition() : null;
+                    final boolean positionChanged =
+                            !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition);
                     if (positionChanged) {
                         startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
                     }
                 } else {
-                    if (leashChanged) {
+                    if (!haveSameLeash(mImeSourceControl, imeSourceControl)) {
                         applyVisibilityToLeash(imeSourceControl);
                     }
                     if (!mImeShowing) {
                         removeImeSurface();
                     }
-                    if (mImeSourceControl != null) {
-                        mImeSourceControl.release(SurfaceControl::release);
-                    }
                 }
-                mImeSourceControl = imeSourceControl;
+            } else if (mAnimation != null) {
+                mAnimation.cancel();
             }
+
+            if (hadImeSourceControl && mImeSourceControl != imeSourceControl) {
+                mImeSourceControl.release(SurfaceControl::release);
+            }
+            mImeSourceControl = imeSourceControl;
         }
 
         private void applyVisibilityToLeash(InsetsSourceControl imeSourceControl) {
@@ -469,6 +478,15 @@
                                 ImeTracker.PHASE_WM_ANIMATION_RUNNING);
                         t.show(mImeSourceControl.getLeash());
                     }
+                    if (DEBUG_IME_VISIBILITY) {
+                        EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START,
+                                statsToken != null ? statsToken.getTag() : TOKEN_NONE,
+                                mDisplayId, mAnimationDirection, alpha, startY , endY,
+                                Objects.toString(mImeSourceControl.getLeash()),
+                                Objects.toString(mImeSourceControl.getInsetsHint()),
+                                Objects.toString(mImeSourceControl.getSurfacePosition()),
+                                Objects.toString(mImeFrame));
+                    }
                     t.apply();
                     mTransactionPool.release(t);
                 }
@@ -476,6 +494,11 @@
                 @Override
                 public void onAnimationCancel(Animator animation) {
                     mCancelled = true;
+                    if (DEBUG_IME_VISIBILITY) {
+                        EventLog.writeEvent(IMF_IME_REMOTE_ANIM_CANCEL,
+                                statsToken != null ? statsToken.getTag() : TOKEN_NONE, mDisplayId,
+                                Objects.toString(mImeSourceControl.getInsetsHint()));
+                    }
                 }
 
                 @Override
@@ -499,6 +522,15 @@
                         ImeTracker.get().onCancelled(mStatsToken,
                                 ImeTracker.PHASE_WM_ANIMATION_RUNNING);
                     }
+                    if (DEBUG_IME_VISIBILITY) {
+                        EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END,
+                                statsToken != null ? statsToken.getTag() : TOKEN_NONE,
+                                mDisplayId, mAnimationDirection, endY,
+                                Objects.toString(mImeSourceControl.getLeash()),
+                                Objects.toString(mImeSourceControl.getInsetsHint()),
+                                Objects.toString(mImeSourceControl.getSurfacePosition()),
+                                Objects.toString(mImeFrame));
+                    }
                     t.apply();
                     mTransactionPool.release(t);
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 039f0e3..fc2a828 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -374,8 +374,6 @@
                         // If this is a transferred starting window, we want it immediately visible.
                         && (change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) {
                     t.setAlpha(leash, 0.f);
-                    // fix alpha in finish transaction in case the animator itself no-ops.
-                    finishT.setAlpha(leash, 1.f);
                 }
             } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
                 finishT.hide(leash);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index b500f5f..b430157 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -282,7 +282,7 @@
         public boolean onTouch(View v, MotionEvent e) {
             boolean isDrag = false;
             int id = v.getId();
-            if (id != R.id.caption_handle && id != R.id.caption) {
+            if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) {
                 return false;
             }
             if (id == R.id.caption_handle) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 467f374..9c2beb9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -132,7 +132,7 @@
 
         mRelayoutParams.reset();
         mRelayoutParams.mRunningTaskInfo = taskInfo;
-        mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration;
+        mRelayoutParams.mLayoutResId = R.layout.desktop_mode_window_decor;
         mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
         mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
         mRelayoutParams.mShadowRadiusId = shadowRadiusID;
@@ -212,7 +212,7 @@
      * Sets up listeners when a new root view is created.
      */
     private void setupRootView() {
-        View caption = mResult.mRootView.findViewById(R.id.caption);
+        View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
         caption.setOnTouchListener(mOnCaptionTouchListener);
         View close = caption.findViewById(R.id.close_window);
         close.setOnClickListener(mOnCaptionButtonClickListener);
@@ -243,7 +243,7 @@
      */
     private void setCaptionVisibility(boolean visible) {
         int v = visible ? View.VISIBLE : View.GONE;
-        View captionView = mResult.mRootView.findViewById(R.id.caption);
+        View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
         captionView.setVisibility(v);
         if (!visible) closeHandleMenu();
     }
@@ -265,7 +265,7 @@
      */
     void setButtonVisibility(boolean visible) {
         int visibility = visible ? View.VISIBLE : View.GONE;
-        View caption = mResult.mRootView.findViewById(R.id.caption);
+        View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
         View back = caption.findViewById(R.id.back_button);
         View close = caption.findViewById(R.id.close_window);
         back.setVisibility(visibility);
@@ -304,7 +304,7 @@
         int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
         int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
         String namePrefix = "Caption Menu";
-        mHandleMenu = addWindow(R.layout.caption_handle_menu, namePrefix, t,
+        mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t,
                 x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY,
                 width, height);
         mSyncQueue.runInSync(transaction -> {
@@ -336,7 +336,7 @@
      */
     void closeHandleMenuIfNeeded(MotionEvent ev) {
         if (isHandleMenuActive()) {
-            if (!checkEventInCaptionView(ev, R.id.caption)) {
+            if (!checkEventInCaptionView(ev, R.id.desktop_mode_caption)) {
                 closeHandleMenu();
             }
         }
@@ -389,7 +389,7 @@
      */
     void checkClickEvent(MotionEvent ev) {
         if (mResult.mRootView == null) return;
-        View caption = mResult.mRootView.findViewById(R.id.caption);
+        View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
         PointF inputPoint = offsetCaptionLocation(ev);
         if (!isHandleMenuActive()) {
             View handle = caption.findViewById(R.id.caption_handle);
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
index 04b1bdd..cd0930c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
@@ -26,6 +26,9 @@
 import com.android.server.wm.flicker.FlickerBuilder
 import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.navBarLayerIsVisibleAtEnd
+import com.android.server.wm.flicker.navBarLayerPositionAtEnd
+import org.junit.Assume
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -91,6 +94,14 @@
         flicker.assertLayersEnd { this.isVisible(testApp) }
     }
 
+    @Postsubmit
+    @Test
+    fun navBarLayerIsVisibleAtEnd() = flicker.navBarLayerIsVisibleAtEnd()
+
+    @Postsubmit
+    @Test
+    fun navBarLayerPositionAtEnd() = flicker.navBarLayerPositionAtEnd()
+
     /** {@inheritDoc} */
     @FlakyTest
     @Test
@@ -98,19 +109,28 @@
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
     /** {@inheritDoc} */
-    @FlakyTest(bugId = 206753786)
+    @Postsubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+    override fun navBarLayerIsVisibleAtStartAndEnd() {
+        Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+        super.navBarLayerIsVisibleAtStartAndEnd()
+    }
 
     /** {@inheritDoc} */
-    @FlakyTest(bugId = 206753786)
+    @Postsubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+    override fun navBarLayerPositionAtStartAndEnd() {
+        Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+        super.navBarLayerPositionAtStartAndEnd()
+    }
 
     /** {@inheritDoc} */
-    @FlakyTest(bugId = 206753786)
+    @Postsubmit
     @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+    override fun navBarWindowIsAlwaysVisible() {
+        Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+        super.navBarWindowIsAlwaysVisible()
+    }
 
     /** {@inheritDoc} */
     @FlakyTest(bugId = 242088970)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 22df362..ffb1a4d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -22,6 +22,8 @@
 import static android.view.WindowInsets.Type.ime;
 
 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.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
@@ -124,6 +126,15 @@
         verify(mT).show(any());
     }
 
+    @Test
+    public void insetsControlChanged_updateImeSourceControl() {
+        mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl());
+        assertNotNull(mPerDisplay.mImeSourceControl);
+
+        mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[]{});
+        assertNull(mPerDisplay.mImeSourceControl);
+    }
+
     private InsetsSourceControl[] insetsSourceControl() {
         return new InsetsSourceControl[]{
                 new InsetsSourceControl(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index d06fb55..7ec4e21 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -30,10 +30,13 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static java.lang.Integer.MAX_VALUE;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -135,12 +138,12 @@
 
     @Test
     public void instantiatePipController_addInitCallback() {
-        verify(mShellInit, times(1)).addInitCallback(any(), any());
+        verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipController));
     }
 
     @Test
     public void instantiateController_registerDumpCallback() {
-        verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), any());
+        verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), eq(mPipController));
     }
 
     @Test
@@ -156,7 +159,7 @@
     @Test
     public void instantiatePipController_registerExternalInterface() {
         verify(mShellController, times(1)).addExternalInterface(
-                eq(ShellSharedConstants.KEY_EXTRA_SHELL_PIP), any(), any());
+                eq(ShellSharedConstants.KEY_EXTRA_SHELL_PIP), any(), eq(mPipController));
     }
 
     @Test
@@ -252,6 +255,10 @@
         final int displayId = 1;
         final Rect bounds = new Rect(0, 0, 10, 10);
         when(mMockPipBoundsAlgorithm.getDefaultBounds()).thenReturn(bounds);
+        when(mMockPipBoundsState.getBounds()).thenReturn(bounds);
+        when(mMockPipBoundsState.getMinSize()).thenReturn(new Point(1, 1));
+        when(mMockPipBoundsState.getMaxSize()).thenReturn(new Point(MAX_VALUE, MAX_VALUE));
+        when(mMockPipBoundsState.getBounds()).thenReturn(bounds);
         when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId);
         when(mMockPipBoundsState.getDisplayLayout()).thenReturn(mMockDisplayLayout1);
         when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
index fbc50c6..8d92d08 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -34,6 +34,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.ShellExecutor;
 
@@ -61,10 +62,9 @@
     @Mock
     private ShellCommandHandler mShellCommandHandler;
     @Mock
-    private ShellExecutor mExecutor;
-    @Mock
     private Context mTestUserContext;
 
+    private TestShellExecutor mExecutor;
     private ShellController mController;
     private TestConfigurationChangeListener mConfigChangeListener;
     private TestKeyguardChangeListener mKeyguardChangeListener;
@@ -77,6 +77,7 @@
         mKeyguardChangeListener = new TestKeyguardChangeListener();
         mConfigChangeListener = new TestConfigurationChangeListener();
         mUserChangeListener = new TestUserChangeListener();
+        mExecutor = new TestShellExecutor();
         mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor);
         mController.onConfigurationChanged(getConfigurationCopy());
     }
@@ -104,6 +105,7 @@
 
         Bundle b = new Bundle();
         mController.asShell().createExternalInterfaces(b);
+        mExecutor.flushAll();
         assertTrue(b.getIBinder(EXTRA_TEST_BINDER) == callback);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index c764741..595c3b4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -936,7 +936,7 @@
         TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
                 RunningTaskInfo taskInfo) {
             final TransitionInfo.Change change =
-                    new TransitionInfo.Change(null /* token */, null /* leash */);
+                    new TransitionInfo.Change(null /* token */, createMockSurface(true));
             change.setMode(mode);
             change.setTaskInfo(taskInfo);
             mInfo.addChange(change);
@@ -961,7 +961,7 @@
         final TransitionInfo.Change mChange;
 
         ChangeBuilder(@WindowManager.TransitionType int mode) {
-            mChange = new TransitionInfo.Change(null /* token */, null /* leash */);
+            mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true));
             mChange.setMode(mode);
         }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
index a5e3a2e..3550721 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
@@ -205,28 +205,32 @@
                     "testEventReceiversOnMultipleDisplays", /*width=*/ 400, /*height=*/ 400,
                     /*densityDpi=*/ 320, surfaceView.getHolder().getSurface(),
                     DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
-            int secondaryDisplayId = secondaryDisplay.getDisplay().getDisplayId();
+            try {
+                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);
+                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);
+                SurfaceControl surfaceControl = mock(SurfaceControl.class);
+                final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+                final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
 
-            mDesktopModeWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT,
-                    finishT);
-            mDesktopModeWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl,
-                    startT, finishT);
-            mDesktopModeWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl,
-                    startT, finishT);
-            mDesktopModeWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo);
-            mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo);
+                mDesktopModeWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT,
+                        finishT);
+                mDesktopModeWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl,
+                        startT, finishT);
+                mDesktopModeWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl,
+                        startT, finishT);
+                mDesktopModeWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo);
+                mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo);
+            } finally {
+                secondaryDisplay.release();
+            }
         });
         verify(mMockInputMonitorFactory, times(2)).create(any(), any());
         verify(mInputMonitor, times(1)).dispose();
@@ -239,7 +243,7 @@
             r.run();
             latch.countDown();
         });
-        latch.await(20, TimeUnit.MILLISECONDS);
+        latch.await(1, TimeUnit.SECONDS);
     }
 
     private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index dd9ab98..ec4f17f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -48,6 +48,7 @@
 import android.view.View;
 import android.view.ViewRootImpl;
 import android.view.WindowManager.LayoutParams;
+import android.window.TaskConstants;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
@@ -232,7 +233,8 @@
         verify(mMockSurfaceControlStartT)
                 .setColor(taskBackgroundSurface, new float[] {1.f, 1.f, 0.f});
         verify(mMockSurfaceControlStartT).setShadowRadius(taskBackgroundSurface, 10);
-        verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, -1);
+        verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface,
+                TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND);
         verify(mMockSurfaceControlStartT).show(taskBackgroundSurface);
 
         verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
@@ -560,7 +562,8 @@
             int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
             String name = "Test Window";
             WindowDecoration.AdditionalWindow additionalWindow =
-                    addWindow(R.layout.caption_handle_menu, name, mMockSurfaceControlAddWindowT,
+                    addWindow(R.layout.desktop_mode_decor_handle_menu, name,
+                            mMockSurfaceControlAddWindowT,
                             x - mRelayoutResult.mDecorContainerOffsetX,
                             y - mRelayoutResult.mDecorContainerOffsetY,
                             width, height);
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index a84c42af..761edf6 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -5855,6 +5855,117 @@
         }
     }
 
+    // Each listener corresponds to a unique callback stub because each listener can subscribe to
+    // different AudioAttributes.
+    private final ConcurrentHashMap<OnDevicesForAttributesChangedListener,
+            IDevicesForAttributesCallbackStub> mDevicesForAttributesListenerToStub =
+                    new ConcurrentHashMap<>();
+
+    private static final class IDevicesForAttributesCallbackStub
+            extends IDevicesForAttributesCallback.Stub {
+        ListenerInfo<OnDevicesForAttributesChangedListener> mInfo;
+
+        IDevicesForAttributesCallbackStub(@NonNull OnDevicesForAttributesChangedListener listener,
+                @NonNull Executor executor) {
+            mInfo = new ListenerInfo<>(listener, executor);
+        }
+
+        public void register(boolean register, AudioAttributes attributes) {
+            try {
+                if (register) {
+                    getService().addOnDevicesForAttributesChangedListener(attributes, this);
+                } else {
+                    getService().removeOnDevicesForAttributesChangedListener(this);
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        @Override
+        public void onDevicesForAttributesChanged(AudioAttributes attributes, boolean forVolume,
+                List<AudioDeviceAttributes> devices) {
+            // forVolume is ignored. The case where it is `true` is not handled.
+            mInfo.mExecutor.execute(() ->
+                    mInfo.mListener.onDevicesForAttributesChanged(
+                            attributes, devices));
+        }
+    }
+
+    /**
+     * @hide
+     * Interface to be notified of when routing changes for the registered audio attributes.
+     */
+    @SystemApi
+    public interface OnDevicesForAttributesChangedListener {
+        /**
+         * Called on the listener to indicate that the audio devices for the given audio
+         * attributes have changed.
+         * @param attributes the {@link AudioAttributes} whose routing changed
+         * @param devices a list of newly routed audio devices
+         */
+        void onDevicesForAttributesChanged(@NonNull AudioAttributes attributes,
+                @NonNull List<AudioDeviceAttributes> devices);
+    }
+
+    /**
+     * @hide
+     * Adds a listener for being notified of routing changes for the given {@link AudioAttributes}.
+     * @param attributes the {@link AudioAttributes} to listen for routing changes
+     * @param executor
+     * @param listener
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MODIFY_AUDIO_ROUTING,
+            android.Manifest.permission.QUERY_AUDIO_STATE
+    })
+    public void addOnDevicesForAttributesChangedListener(@NonNull AudioAttributes attributes,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnDevicesForAttributesChangedListener listener) {
+        Objects.requireNonNull(attributes);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+
+        synchronized (mDevicesForAttributesListenerToStub) {
+            IDevicesForAttributesCallbackStub callbackStub =
+                    mDevicesForAttributesListenerToStub.get(listener);
+
+            if (callbackStub == null) {
+                callbackStub = new IDevicesForAttributesCallbackStub(listener, executor);
+                mDevicesForAttributesListenerToStub.put(listener, callbackStub);
+            }
+
+            callbackStub.register(true, attributes);
+        }
+    }
+
+    /**
+     * @hide
+     * Removes a previously registered listener for being notified of routing changes for the given
+     * {@link AudioAttributes}.
+     * @param listener
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MODIFY_AUDIO_ROUTING,
+            android.Manifest.permission.QUERY_AUDIO_STATE
+    })
+    public void removeOnDevicesForAttributesChangedListener(
+            @NonNull OnDevicesForAttributesChangedListener listener) {
+        Objects.requireNonNull(listener);
+
+        synchronized (mDevicesForAttributesListenerToStub) {
+            IDevicesForAttributesCallbackStub callbackStub =
+                    mDevicesForAttributesListenerToStub.get(listener);
+            if (callbackStub != null) {
+                callbackStub.register(false, null /* attributes */);
+            }
+
+            mDevicesForAttributesListenerToStub.remove(listener);
+        }
+    }
+
     /**
      * Get the audio devices that would be used for the routing of the given audio attributes.
      * These are the devices anticipated to play sound from an {@link AudioTrack} created with
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 0de367d..5ee32d6 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -36,6 +36,7 @@
 import android.media.ICapturePresetDevicesRoleDispatcher;
 import android.media.ICommunicationDeviceDispatcher;
 import android.media.IDeviceVolumeBehaviorDispatcher;
+import android.media.IDevicesForAttributesCallback;
 import android.media.IMuteAwaitConnectionCallback;
 import android.media.IPlaybackConfigDispatcher;
 import android.media.IPreferredMixerAttributesDispatcher;
@@ -356,6 +357,12 @@
 
     List<AudioDeviceAttributes> getDevicesForAttributesUnprotected(in AudioAttributes attributes);
 
+    void addOnDevicesForAttributesChangedListener(in AudioAttributes attributes,
+            in IDevicesForAttributesCallback callback);
+
+    oneway void removeOnDevicesForAttributesChangedListener(
+            in IDevicesForAttributesCallback callback);
+
     int setAllowedCapturePolicy(in int capturePolicy);
 
     int getAllowedCapturePolicy();
diff --git a/media/java/android/media/IDevicesForAttributesCallback.aidl b/media/java/android/media/IDevicesForAttributesCallback.aidl
new file mode 100644
index 0000000..489ecf6
--- /dev/null
+++ b/media/java/android/media/IDevicesForAttributesCallback.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+
+/**
+ * AIDL for AudioService to signal updates of audio devices routing for attributes.
+ *
+ * {@hide}
+ */
+oneway interface IDevicesForAttributesCallback {
+
+    void onDevicesForAttributesChanged(in AudioAttributes attributes, boolean forVolume,
+            in List<AudioDeviceAttributes> devices);
+
+}
+
diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java
index 32a2ad3..9325999 100644
--- a/media/java/android/media/ImageWriter.java
+++ b/media/java/android/media/ImageWriter.java
@@ -431,17 +431,15 @@
      * @see Image#close
      */
     public Image dequeueInputImage() {
-        synchronized (mCloseLock) {
-            if (mDequeuedImages.size() >= mMaxImages) {
-                throw new IllegalStateException(
-                        "Already dequeued max number of Images " + mMaxImages);
-            }
-            WriterSurfaceImage newImage = new WriterSurfaceImage(this);
-            nativeDequeueInputImage(mNativeContext, newImage);
-            mDequeuedImages.add(newImage);
-            newImage.mIsImageValid = true;
-            return newImage;
+        if (mDequeuedImages.size() >= mMaxImages) {
+            throw new IllegalStateException(
+                    "Already dequeued max number of Images " + mMaxImages);
         }
+        WriterSurfaceImage newImage = new WriterSurfaceImage(this);
+        nativeDequeueInputImage(mNativeContext, newImage);
+        mDequeuedImages.add(newImage);
+        newImage.mIsImageValid = true;
+        return newImage;
     }
 
     /**
@@ -500,52 +498,50 @@
             throw new IllegalArgumentException("image shouldn't be null");
         }
 
-        synchronized (mCloseLock) {
-            boolean ownedByMe = isImageOwnedByMe(image);
-            if (ownedByMe && !(((WriterSurfaceImage) image).mIsImageValid)) {
-                throw new IllegalStateException("Image from ImageWriter is invalid");
+        boolean ownedByMe = isImageOwnedByMe(image);
+        if (ownedByMe && !(((WriterSurfaceImage) image).mIsImageValid)) {
+            throw new IllegalStateException("Image from ImageWriter is invalid");
+        }
+
+        // For images from other components that have non-null owner, need to detach first,
+        // then attach. Images without owners must already be attachable.
+        if (!ownedByMe) {
+            if ((image.getOwner() instanceof ImageReader)) {
+                ImageReader prevOwner = (ImageReader) image.getOwner();
+
+                prevOwner.detachImage(image);
+            } else if (image.getOwner() != null) {
+                throw new IllegalArgumentException(
+                        "Only images from ImageReader can be queued to"
+                                + " ImageWriter, other image source is not supported yet!");
             }
 
-            // For images from other components that have non-null owner, need to detach first,
-            // then attach. Images without owners must already be attachable.
-            if (!ownedByMe) {
-                if ((image.getOwner() instanceof ImageReader)) {
-                    ImageReader prevOwner = (ImageReader) image.getOwner();
+            attachAndQueueInputImage(image);
+            // This clears the native reference held by the original owner.
+            // When this Image is detached later by this ImageWriter, the
+            // native memory won't be leaked.
+            image.close();
+            return;
+        }
 
-                    prevOwner.detachImage(image);
-                } else if (image.getOwner() != null) {
-                    throw new IllegalArgumentException(
-                            "Only images from ImageReader can be queued to"
-                                    + " ImageWriter, other image source is not supported yet!");
-                }
+        Rect crop = image.getCropRect();
+        nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), image.getDataSpace(),
+                crop.left, crop.top, crop.right, crop.bottom, image.getTransform(),
+                image.getScalingMode());
 
-                attachAndQueueInputImage(image);
-                // This clears the native reference held by the original owner.
-                // When this Image is detached later by this ImageWriter, the
-                // native memory won't be leaked.
-                image.close();
-                return;
-            }
-
-            Rect crop = image.getCropRect();
-            nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), image.getDataSpace(),
-                    crop.left, crop.top, crop.right, crop.bottom, image.getTransform(),
-                    image.getScalingMode());
-
-            /**
-             * Only remove and cleanup the Images that are owned by this
-             * ImageWriter. Images detached from other owners are only temporarily
-             * owned by this ImageWriter and will be detached immediately after they
-             * are released by downstream consumers, so there is no need to keep
-             * track of them in mDequeuedImages.
-             */
-            if (ownedByMe) {
-                mDequeuedImages.remove(image);
-                // Do not call close here, as close is essentially cancel image.
-                WriterSurfaceImage wi = (WriterSurfaceImage) image;
-                wi.clearSurfacePlanes();
-                wi.mIsImageValid = false;
-            }
+        /**
+         * Only remove and cleanup the Images that are owned by this
+         * ImageWriter. Images detached from other owners are only temporarily
+         * owned by this ImageWriter and will be detached immediately after they
+         * are released by downstream consumers, so there is no need to keep
+         * track of them in mDequeuedImages.
+         */
+        if (ownedByMe) {
+            mDequeuedImages.remove(image);
+            // Do not call close here, as close is essentially cancel image.
+            WriterSurfaceImage wi = (WriterSurfaceImage) image;
+            wi.clearSurfacePlanes();
+            wi.mIsImageValid = false;
         }
     }
 
@@ -681,11 +677,11 @@
      */
     @Override
     public void close() {
+        setOnImageReleasedListener(null, null);
         synchronized (mCloseLock) {
             if (!mIsWriterValid) {
                 return;
             }
-            setOnImageReleasedListener(null, null);
             for (Image image : mDequeuedImages) {
                 image.close();
             }
@@ -817,14 +813,12 @@
         }
 
         final Handler handler;
-        final boolean isWriterValid;
         synchronized (iw.mListenerLock) {
             handler = iw.mListenerHandler;
         }
-        synchronized (iw.mCloseLock) {
-            isWriterValid = iw.mIsWriterValid;
-        }
-        if (handler != null && isWriterValid) {
+
+        if (handler != null) {
+            // The ListenerHandler will take care of ensuring that the parent ImageWriter is valid
             handler.sendEmptyMessage(0);
         }
     }
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 58078cf..aacea3d 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -609,6 +609,7 @@
     jobject obj = env->NewObject(eventClazz, eventInit, tableId, version, sectionNum, dataLength);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getMediaEvent(jobjectArray &arr, const int size,
@@ -639,6 +640,7 @@
 
         audioDescriptor = env->NewObject(adClazz, adInit, adFade, adPan, versionTextTag,
                                          adGainCenter, adGainFront, adGainSurround);
+        env->DeleteLocalRef(adClazz);
     }
 
     jlong dataLength = mediaEvent.dataLength;
@@ -685,6 +687,7 @@
         env->DeleteLocalRef(audioDescriptor);
     }
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getPesEvent(jobjectArray &arr, const int size,
@@ -701,6 +704,7 @@
     jobject obj = env->NewObject(eventClazz, eventInit, streamId, dataLength, mpuSequenceNumber);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getTsRecordEvent(jobjectArray &arr, const int size,
@@ -741,6 +745,7 @@
             env->NewObject(eventClazz, eventInit, jpid, ts, sc, byteNumber, pts, firstMbInSlice);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getMmtpRecordEvent(jobjectArray &arr, const int size,
@@ -762,6 +767,7 @@
                                  mpuSequenceNumber, pts, firstMbInSlice, tsIndexMask);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getDownloadEvent(jobjectArray &arr, const int size,
@@ -782,6 +788,7 @@
                                  itemFragmentIndex, lastItemFragmentIndex, dataLength);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getIpPayloadEvent(jobjectArray &arr, const int size,
@@ -795,6 +802,7 @@
     jobject obj = env->NewObject(eventClazz, eventInit, dataLength);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getTemiEvent(jobjectArray &arr, const int size,
@@ -815,6 +823,7 @@
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(array);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getScramblingStatusEvent(jobjectArray &arr, const int size,
@@ -829,6 +838,7 @@
     jobject obj = env->NewObject(eventClazz, eventInit, scramblingStatus);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getIpCidChangeEvent(jobjectArray &arr, const int size,
@@ -842,6 +852,7 @@
     jobject obj = env->NewObject(eventClazz, eventInit, cid);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::getRestartEvent(jobjectArray &arr, const int size,
@@ -854,6 +865,7 @@
     jobject obj = env->NewObject(eventClazz, eventInit, startId);
     env->SetObjectArrayElement(arr, size, obj);
     env->DeleteLocalRef(obj);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::onFilterEvent(const vector<DemuxFilterEvent> &events) {
@@ -952,6 +964,7 @@
               "Filter object has been freed. Ignoring callback.");
     }
     env->DeleteLocalRef(array);
+    env->DeleteLocalRef(eventClazz);
 }
 
 void FilterClientCallbackImpl::onFilterStatus(const DemuxFilterStatus status) {
@@ -1058,6 +1071,7 @@
         executeOnScanMessage(env, clazz, frontend, type, message);
         env->DeleteLocalRef(frontend);
     }
+    env->DeleteLocalRef(clazz);
 }
 
 void FrontendClientCallbackImpl::executeOnScanMessage(
@@ -1184,6 +1198,7 @@
                                                  "Atsc3PlpInfo;)V"),
                                 array);
             env->DeleteLocalRef(array);
+            env->DeleteLocalRef(plpClazz);
             break;
         }
         case FrontendScanMessageType::MODULATION: {
@@ -2195,6 +2210,7 @@
                                        static_cast<long>(s.get<FrontendStatus::Tag::innerFec>()));
                 env->SetObjectField(statusObj, field, newLongObj);
                 env->DeleteLocalRef(newLongObj);
+                env->DeleteLocalRef(longClazz);
                 break;
             }
             case FrontendStatus::Tag::modulationStatus: {
@@ -2360,6 +2376,7 @@
 
                 env->SetObjectField(statusObj, field, valObj);
                 env->DeleteLocalRef(valObj);
+                env->DeleteLocalRef(plpClazz);
                 break;
             }
             case FrontendStatus::Tag::modulations: {
@@ -2770,6 +2787,7 @@
 
                 env->SetObjectField(statusObj, field, valObj);
                 env->DeleteLocalRef(valObj);
+                env->DeleteLocalRef(plpClazz);
                 break;
             }
         }
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml
index 244b367..51fc7ed 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml
@@ -18,6 +18,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.settingslib.widget">
 
-    <uses-sdk android:minSdkVersion="29" />
+    <uses-sdk android:minSdkVersion="21" />
 
 </manifest>
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout/collapsing_toolbar_base_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout/collapsing_toolbar_base_layout.xml
index c799b99..02f69f6 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout/collapsing_toolbar_base_layout.xml
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout/collapsing_toolbar_base_layout.xml
@@ -29,6 +29,12 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:theme="?android:attr/actionBarTheme" />
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/support_action_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="?android:attr/actionBarTheme"
+        android:visibility="gone" />
     <FrameLayout
         android:id="@+id/content_frame"
         android:layout_width="match_parent"
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
new file mode 100644
index 0000000..dcc6e5a
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.collapsingtoolbar;
+
+import android.app.ActionBar;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toolbar;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.android.settingslib.utils.BuildCompatUtils;
+import com.android.settingslib.widget.R;
+
+import com.google.android.material.appbar.AppBarLayout;
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+import com.google.android.material.color.DynamicColors;
+
+/**
+ * A base Activity that has a collapsing toolbar layout is used for the activities intending to
+ * enable the collapsing toolbar function.
+ */
+public class CollapsingToolbarAppCompatActivity extends AppCompatActivity {
+
+    private class DelegateCallback implements CollapsingToolbarDelegate.HostCallback {
+        @Nullable
+        @Override
+        public ActionBar setActionBar(Toolbar toolbar) {
+            return null;
+        }
+
+        @Nullable
+        @Override
+        public androidx.appcompat.app.ActionBar setActionBar(
+                androidx.appcompat.widget.Toolbar toolbar) {
+            CollapsingToolbarAppCompatActivity.super.setSupportActionBar(toolbar);
+            return CollapsingToolbarAppCompatActivity.super.getSupportActionBar();
+        }
+
+        @Override
+        public void setOuterTitle(CharSequence title) {
+            CollapsingToolbarAppCompatActivity.super.setTitle(title);
+        }
+    }
+
+    private CollapsingToolbarDelegate mToolbardelegate;
+
+    private int mCustomizeLayoutResId = 0;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (BuildCompatUtils.isAtLeastS()) {
+            DynamicColors.applyToActivityIfAvailable(this);
+        }
+        setTheme(R.style.Theme_SubSettingsBase);
+
+        if (mCustomizeLayoutResId > 0 && !BuildCompatUtils.isAtLeastS()) {
+            super.setContentView(mCustomizeLayoutResId);
+            return;
+        }
+
+        View view = getToolbarDelegate().onCreateView(getLayoutInflater(), null, this);
+        super.setContentView(view);
+    }
+
+    @Override
+    public void setContentView(int layoutResID) {
+        final ViewGroup parent = (mToolbardelegate == null) ? findViewById(R.id.content_frame)
+                : mToolbardelegate.getContentFrameLayout();
+        if (parent != null) {
+            parent.removeAllViews();
+        }
+        LayoutInflater.from(this).inflate(layoutResID, parent);
+    }
+
+    @Override
+    public void setContentView(View view) {
+        final ViewGroup parent = (mToolbardelegate == null) ? findViewById(R.id.content_frame)
+                : mToolbardelegate.getContentFrameLayout();
+        if (parent != null) {
+            parent.addView(view);
+        }
+    }
+
+    @Override
+    public void setContentView(View view, ViewGroup.LayoutParams params) {
+        final ViewGroup parent = (mToolbardelegate == null) ? findViewById(R.id.content_frame)
+                : mToolbardelegate.getContentFrameLayout();
+        if (parent != null) {
+            parent.addView(view, params);
+        }
+    }
+
+    /**
+     * This method allows an activity to replace the default layout with a customize layout. Notice
+     * that it will no longer apply the features being provided by this class when this method
+     * gets called.
+     */
+    protected void setCustomizeContentView(int layoutResId) {
+        mCustomizeLayoutResId = layoutResId;
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        getToolbarDelegate().setTitle(title);
+    }
+
+    @Override
+    public void setTitle(int titleId) {
+        setTitle(getText(titleId));
+    }
+
+    @Override
+    public boolean onSupportNavigateUp() {
+        if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
+            getSupportFragmentManager().popBackStackImmediate();
+        }
+
+        // Closes the activity if there is no fragment inside the stack. Otherwise the activity will
+        // has a blank screen since there is no any fragment.
+        if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
+            finishAfterTransition();
+        }
+        return true;
+    }
+
+    @Override
+    public void onBackPressed() {
+        super.onBackPressed();
+
+        // Closes the activity if there is no fragment inside the stack. Otherwise the activity will
+        // has a blank screen since there is no any fragment. onBackPressed() in Activity.java only
+        // handles popBackStackImmediate(). This will close activity to avoid a blank screen.
+        if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
+            finishAfterTransition();
+        }
+    }
+
+    /**
+     * Returns an instance of collapsing toolbar.
+     */
+    @Nullable
+    public CollapsingToolbarLayout getCollapsingToolbarLayout() {
+        return getToolbarDelegate().getCollapsingToolbarLayout();
+    }
+
+    /**
+     * Return an instance of app bar.
+     */
+    @Nullable
+    public AppBarLayout getAppBarLayout() {
+        return getToolbarDelegate().getAppBarLayout();
+    }
+
+    private CollapsingToolbarDelegate getToolbarDelegate() {
+        if (mToolbardelegate == null) {
+            mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback());
+        }
+        return mToolbardelegate;
+    }
+}
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
index 8c8b478..01f92c4 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
@@ -117,12 +117,30 @@
 
     @Override
     public boolean onNavigateUp() {
-        if (!super.onNavigateUp()) {
+        if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
+            getSupportFragmentManager().popBackStackImmediate();
+        }
+
+        // Closes the activity if there is no fragment inside the stack. Otherwise the activity will
+        // has a blank screen since there is no any fragment.
+        if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
             finishAfterTransition();
         }
         return true;
     }
 
+    @Override
+    public void onBackPressed() {
+        super.onBackPressed();
+
+        // Closes the activity if there is no fragment inside the stack. Otherwise the activity will
+        // has a blank screen since there is no any fragment. onBackPressed() in Activity.java only
+        // handles popBackStackImmediate(). This will close activity to avoid a blank screen.
+        if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
+            finishAfterTransition();
+        }
+    }
+
     /**
      * Returns an instance of collapsing toolbar.
      */
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
index 01698b7..1c2288a 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
@@ -19,9 +19,11 @@
 import static android.text.Layout.HYPHENATION_FREQUENCY_NORMAL_FAST;
 
 import android.app.ActionBar;
+import android.app.Activity;
 import android.content.res.Configuration;
 import android.graphics.text.LineBreakConfig;
 import android.os.Build;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -30,6 +32,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
 
 import com.android.settingslib.widget.R;
@@ -42,7 +45,7 @@
  * extend from {@link CollapsingToolbarBaseActivity} or from {@link CollapsingToolbarBaseFragment}.
  */
 public class CollapsingToolbarDelegate {
-
+    private static final String TAG = "CTBdelegate";
     /** Interface to be implemented by the host of the Collapsing Toolbar. */
     public interface HostCallback {
         /**
@@ -53,6 +56,13 @@
         @Nullable
         ActionBar setActionBar(Toolbar toolbar);
 
+        /** Sets support tool bar and return support action bar, this is for AppCompatActivity. */
+        @Nullable
+        default androidx.appcompat.app.ActionBar setActionBar(
+                androidx.appcompat.widget.Toolbar toolbar) {
+            return null;
+        }
+
         /** Sets a title on the host. */
         void setOuterTitle(CharSequence title);
     }
@@ -79,6 +89,13 @@
     /** Method to call that creates the root view of the collapsing toolbar. */
     @SuppressWarnings("RestrictTo")
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) {
+        return onCreateView(inflater, container, null);
+    }
+
+    /** Method to call that creates the root view of the collapsing toolbar. */
+    @SuppressWarnings("RestrictTo")
+    View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            Activity activity) {
         final View view =
                 inflater.inflate(R.layout.collapsing_toolbar_base_layout, container, false);
         if (view instanceof CoordinatorLayout) {
@@ -99,17 +116,57 @@
             }
         }
         autoSetCollapsingToolbarLayoutScrolling();
-        mToolbar = view.findViewById(R.id.action_bar);
         mContentFrameLayout = view.findViewById(R.id.content_frame);
-        final ActionBar actionBar = mHostCallback.setActionBar(mToolbar);
+        if (activity instanceof AppCompatActivity) {
+            Log.d(TAG, "onCreateView: from AppCompatActivity and sub-class.");
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                initSupportActionBar(inflater);
+            } else {
+                initRSupportActionBar(view);
+            }
+        } else {
+            Log.d(TAG, "onCreateView: from NonAppCompatActivity.");
+            mToolbar = view.findViewById(R.id.action_bar);
+            final ActionBar actionBar = mHostCallback.setActionBar(mToolbar);
+            // Enable title and home button by default
+            if (actionBar != null) {
+                actionBar.setDisplayHomeAsUpEnabled(true);
+                actionBar.setHomeButtonEnabled(true);
+                actionBar.setDisplayShowTitleEnabled(true);
+            }
+        }
+        return view;
+    }
 
-        // Enable title and home button by default
+    private void initSupportActionBar(@NonNull LayoutInflater inflater) {
+        if (mCollapsingToolbarLayout == null) {
+            return;
+        }
+        mCollapsingToolbarLayout.removeAllViews();
+        inflater.inflate(R.layout.support_toolbar, mCollapsingToolbarLayout);
+        final androidx.appcompat.widget.Toolbar supportToolbar =
+                mCollapsingToolbarLayout.findViewById(R.id.support_action_bar);
+        final androidx.appcompat.app.ActionBar actionBar =
+                mHostCallback.setActionBar(supportToolbar);
         if (actionBar != null) {
             actionBar.setDisplayHomeAsUpEnabled(true);
             actionBar.setHomeButtonEnabled(true);
             actionBar.setDisplayShowTitleEnabled(true);
         }
-        return view;
+    }
+
+    private void initRSupportActionBar(View view) {
+        view.findViewById(R.id.action_bar).setVisibility(View.GONE);
+        final androidx.appcompat.widget.Toolbar supportToolbar =
+                view.findViewById(R.id.support_action_bar);
+        supportToolbar.setVisibility(View.VISIBLE);
+        final androidx.appcompat.app.ActionBar actionBar =
+                mHostCallback.setActionBar(supportToolbar);
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+            actionBar.setHomeButtonEnabled(true);
+            actionBar.setDisplayShowTitleEnabled(true);
+        }
     }
 
     /** Return an instance of CoordinatorLayout. */
@@ -160,9 +217,13 @@
                 new AppBarLayout.Behavior.DragCallback() {
                     @Override
                     public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
-                        // Header can be scrolling while device in landscape mode.
-                        return appBarLayout.getResources().getConfiguration().orientation
-                                == Configuration.ORIENTATION_LANDSCAPE;
+                        // Header can be scrolling while device in landscape mode and SDK > 33
+                        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+                            return false;
+                        } else {
+                            return appBarLayout.getResources().getConfiguration().orientation
+                                    == Configuration.ORIENTATION_LANDSCAPE;
+                        }
                     }
                 });
         params.setBehavior(behavior);
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
index d67ac3b..e4e34f8 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
@@ -49,7 +49,7 @@
  */
 @RequiresApi(Build.VERSION_CODES.S)
 public class CollapsingCoordinatorLayout extends CoordinatorLayout {
-    private static final String TAG = "CollapsingCoordinatorLayout";
+    private static final String TAG = "CollapsingCoordinator";
     private static final float TOOLBAR_LINE_SPACING_MULTIPLIER = 1.1f;
 
     private CharSequence mToolbarTitle;
@@ -255,9 +255,13 @@
                 new AppBarLayout.Behavior.DragCallback() {
                     @Override
                     public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
-                        // Header can be scrolling while device in landscape mode.
-                        return appBarLayout.getResources().getConfiguration().orientation
-                                == Configuration.ORIENTATION_LANDSCAPE;
+                        // Header can be scrolling while device in landscape mode and SDK > 33
+                        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+                            return false;
+                        } else {
+                            return appBarLayout.getResources().getConfiguration().orientation
+                                    == Configuration.ORIENTATION_LANDSCAPE;
+                        }
                     }
                 });
         params.setBehavior(behavior);
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
index 59ae122..b39d09f 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
@@ -18,7 +18,11 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_height="wrap_content"
-    android:layout_width="match_parent">
+    android:layout_width="match_parent"
+    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
 
     <TextView
         android:id="@+id/switch_text"
@@ -50,7 +54,6 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical"
-        android:layout_marginEnd="@dimen/settingslib_switchbar_subsettings_margin_end"
         android:focusable="false"
         android:clickable="false"
         android:theme="@style/SwitchBar.Switch.Settingslib"/>
diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-sw600dp/dmiens.xml b/packages/SettingsLib/MainSwitchPreference/res/values-sw600dp/dmiens.xml
deleted file mode 100644
index 55a2589..0000000
--- a/packages/SettingsLib/MainSwitchPreference/res/values-sw600dp/dmiens.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2021 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<resources>
-
-    <!-- SwitchBar sub settings margin start / end -->
-    <dimen name="settingslib_switchbar_subsettings_margin_start">80dp</dimen>
-</resources>
diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp-land/dmiens.xml b/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp-land/dmiens.xml
deleted file mode 100644
index 53995bc..0000000
--- a/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp-land/dmiens.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2021 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<resources>
-
-    <!-- SwitchBar sub settings margin start / end -->
-    <dimen name="settingslib_switchbar_subsettings_margin_start">128dp</dimen>
-    <dimen name="settingslib_switchbar_subsettings_margin_end">128dp</dimen>
-</resources>
diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp/dmiens.xml b/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp/dmiens.xml
deleted file mode 100644
index 9015c58..0000000
--- a/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp/dmiens.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2021 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<resources>
-
-    <!-- SwitchBar sub settings margin start / end -->
-    <dimen name="settingslib_switchbar_subsettings_margin_start">80dp</dimen>
-    <dimen name="settingslib_switchbar_subsettings_margin_end">80dp</dimen>
-</resources>
diff --git a/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml b/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml
index 157a54e..88b2c87 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml
@@ -27,6 +27,6 @@
     <dimen name="settingslib_switch_title_margin">24dp</dimen>
 
     <!-- SwitchBar sub settings margin start / end -->
-    <dimen name="settingslib_switchbar_subsettings_margin_start">72dp</dimen>
+    <dimen name="settingslib_switchbar_subsettings_margin_start">56dp</dimen>
     <dimen name="settingslib_switchbar_subsettings_margin_end">16dp</dimen>
 </resources>
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
index 86fec50..864a8bb 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
@@ -161,6 +161,19 @@
     }
 
     /**
+     * Set icon space reserved for title
+     */
+    public void setIconSpaceReserved(boolean iconSpaceReserved) {
+        if (mTextView != null && !BuildCompatUtils.isAtLeastS()) {
+            LayoutParams params = (LayoutParams) mTextView.getLayoutParams();
+            int iconSpace = getContext().getResources().getDimensionPixelSize(
+                    R.dimen.settingslib_switchbar_subsettings_margin_start);
+            params.setMarginStart(iconSpaceReserved ? iconSpace : 0);
+            mTextView.setLayoutParams(params);
+        }
+    }
+
+    /**
      * Show the MainSwitchBar
      */
     public void show() {
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index fc0e05f..53cc268 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -37,7 +37,6 @@
     private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
 
     private MainSwitchBar mMainSwitchBar;
-    private CharSequence mTitle;
 
     public MainSwitchPreference(Context context) {
         super(context);
@@ -68,6 +67,10 @@
         holder.setDividerAllowedBelow(false);
 
         mMainSwitchBar = (MainSwitchBar) holder.findViewById(R.id.settingslib_main_switch_bar);
+        // To support onPreferenceChange callback, it needs to call callChangeListener() when
+        // MainSwitchBar is clicked.
+        mMainSwitchBar.setOnClickListener((view) -> callChangeListener(isChecked()));
+        setIconSpaceReserved(isIconSpaceReserved());
         updateStatus(isChecked());
         registerListenerToSwitchBar();
     }
@@ -82,6 +85,10 @@
             final CharSequence title = a.getText(
                     androidx.preference.R.styleable.Preference_android_title);
             setTitle(title);
+
+            final boolean bIconSpaceReserved = a.getBoolean(
+                    androidx.preference.R.styleable.Preference_android_iconSpaceReserved, true);
+            setIconSpaceReserved(bIconSpaceReserved);
             a.recycle();
         }
     }
@@ -96,9 +103,17 @@
 
     @Override
     public void setTitle(CharSequence title) {
-        mTitle = title;
+        super.setTitle(title);
         if (mMainSwitchBar != null) {
-            mMainSwitchBar.setTitle(mTitle);
+            mMainSwitchBar.setTitle(title);
+        }
+    }
+
+    @Override
+    public void setIconSpaceReserved(boolean iconSpaceReserved) {
+        super.setIconSpaceReserved(iconSpaceReserved);
+        if (mMainSwitchBar != null) {
+            mMainSwitchBar.setIconSpaceReserved(iconSpaceReserved);
         }
     }
 
@@ -113,7 +128,7 @@
     public void updateStatus(boolean checked) {
         setChecked(checked);
         if (mMainSwitchBar != null) {
-            mMainSwitchBar.setTitle(mTitle);
+            mMainSwitchBar.setTitle(getTitle());
             mMainSwitchBar.show();
         }
     }
@@ -125,6 +140,7 @@
         if (!mSwitchChangeListeners.contains(listener)) {
             mSwitchChangeListeners.add(listener);
         }
+
         if (mMainSwitchBar != null) {
             mMainSwitchBar.addOnSwitchChangeListener(listener);
         }
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v31/style_preference.xml b/packages/SettingsLib/SettingsTheme/res/values-v31/style_preference.xml
index bda478e..f1e028b 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v31/style_preference.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v31/style_preference.xml
@@ -17,6 +17,7 @@
 <resources>
     <style name="PreferenceTheme.SettingsLib" parent="@style/PreferenceThemeOverlay">
         <item name="preferenceCategoryTitleTextAppearance">@style/TextAppearance.CategoryTitle.SettingsLib</item>
+        <item name="preferenceScreenStyle">@style/SettingsPreferenceScreen.SettingsLib</item>
         <item name="preferenceCategoryStyle">@style/SettingsCategoryPreference.SettingsLib</item>
         <item name="preferenceStyle">@style/SettingsPreference.SettingsLib</item>
         <item name="checkBoxPreferenceStyle">@style/SettingsCheckBoxPreference.SettingsLib</item>
@@ -28,6 +29,11 @@
         <item name="footerPreferenceStyle">@style/Preference.Material</item>
     </style>
 
+    <style name="SettingsPreferenceScreen.SettingsLib" parent="@style/Preference.PreferenceScreen.Material">
+        <item name="layout">@layout/settingslib_preference</item>
+        <item name="iconSpaceReserved">@bool/settingslib_config_icon_space_reserved</item>
+    </style>
+
     <style name="SettingsCategoryPreference.SettingsLib" parent="@style/Preference.Category.Material">
         <item name="iconSpaceReserved">@bool/settingslib_config_icon_space_reserved</item>
         <item name="allowDividerAbove">@bool/settingslib_config_allow_divider</item>
diff --git a/packages/SettingsLib/SettingsTheme/res/values/styles.xml b/packages/SettingsLib/SettingsTheme/res/values/styles.xml
index 328ab46..af3fc48 100644
--- a/packages/SettingsLib/SettingsTheme/res/values/styles.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values/styles.xml
@@ -28,19 +28,19 @@
     </style>
 
     <style name="TextAppearance.TopIntroText"
-        parent="@android:style/TextAppearance.DeviceDefault">
+           parent="@android:style/TextAppearance.DeviceDefault">
         <item name="android:textSize">14sp</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
     <style name="TextAppearance.EntityHeaderTitle"
-        parent="@android:style/TextAppearance.DeviceDefault.WindowTitle">
+           parent="@android:style/TextAppearance.DeviceDefault.WindowTitle">
         <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textSize">20sp</item>
     </style>
 
     <style name="TextAppearance.EntityHeaderSummary"
-        parent="@android:style/TextAppearance.DeviceDefault">
+           parent="@android:style/TextAppearance.DeviceDefault">
         <item name="android:textAlignment">viewStart</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
         <item name="android:singleLine">true</item>
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 40613ce..139f3e1 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -27,6 +27,7 @@
         "androidx.slice_slice-builders",
         "androidx.slice_slice-core",
         "androidx.slice_slice-view",
+        "androidx.compose.animation_animation",
         "androidx.compose.material3_material3",
         "androidx.compose.material_material-icons-extended",
         "androidx.compose.runtime_runtime",
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index aa10cc8..a81e2e3 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalAnimationApi::class)
+
 package com.android.settingslib.spa.framework
 
 import android.content.Intent
@@ -21,25 +23,31 @@
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.unit.IntOffset
 import androidx.core.view.WindowCompat
 import androidx.navigation.NavGraph.Companion.findStartDestination
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
 import com.android.settingslib.spa.R
 import com.android.settingslib.spa.framework.common.LogCategory
 import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.compose.AnimatedNavHost
 import com.android.settingslib.spa.framework.compose.LocalNavController
 import com.android.settingslib.spa.framework.compose.NavControllerWrapperImpl
+import com.android.settingslib.spa.framework.compose.composable
 import com.android.settingslib.spa.framework.compose.localNavController
+import com.android.settingslib.spa.framework.compose.rememberAnimatedNavController
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.framework.util.PageEvent
 import com.android.settingslib.spa.framework.util.getDestination
@@ -86,7 +94,7 @@
 @VisibleForTesting
 @Composable
 fun BrowseContent(sppRepository: SettingsPageProviderRepository, initialIntent: Intent? = null) {
-    val navController = rememberNavController()
+    val navController = rememberAnimatedNavController()
     CompositionLocalProvider(navController.localNavController()) {
         val controller = LocalNavController.current as NavControllerWrapperImpl
         controller.NavContent(sppRepository.getAllProviders())
@@ -97,15 +105,41 @@
 @Composable
 private fun NavControllerWrapperImpl.NavContent(allProvider: Collection<SettingsPageProvider>) {
     val nullPage = SettingsPage.createNull()
-    NavHost(
+    AnimatedNavHost(
         navController = navController,
         startDestination = nullPage.sppName,
     ) {
+        val slideEffect = tween<IntOffset>(durationMillis = 300)
+        val fadeEffect = tween<Float>(durationMillis = 300)
         composable(nullPage.sppName) {}
         for (spp in allProvider) {
             composable(
                 route = spp.name + spp.parameter.navRoute(),
                 arguments = spp.parameter,
+                enterTransition = {
+                    slideIntoContainer(
+                        AnimatedContentScope.SlideDirection.Left,
+                        animationSpec = slideEffect
+                    ) + fadeIn(animationSpec = fadeEffect)
+                },
+                exitTransition = {
+                    slideOutOfContainer(
+                        AnimatedContentScope.SlideDirection.Left,
+                        animationSpec = slideEffect
+                    ) + fadeOut(animationSpec = fadeEffect)
+                },
+                popEnterTransition = {
+                    slideIntoContainer(
+                        AnimatedContentScope.SlideDirection.Right,
+                        animationSpec = slideEffect
+                    ) + fadeIn(animationSpec = fadeEffect)
+                },
+                popExitTransition = {
+                    slideOutOfContainer(
+                        AnimatedContentScope.SlideDirection.Right,
+                        animationSpec = slideEffect
+                    ) + fadeOut(animationSpec = fadeEffect)
+                },
             ) { navBackStackEntry ->
                 spp.PageEvent(navBackStackEntry.arguments)
                 spp.Page(navBackStackEntry.arguments)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt
new file mode 100644
index 0000000..930a83f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDestination
+import androidx.navigation.NavOptions
+import androidx.navigation.Navigator
+
+/**
+ * Navigator that navigates through [Composable]s. Every destination using this Navigator must
+ * set a valid [Composable] by setting it directly on an instantiated [Destination] or calling
+ * [composable].
+ */
+@ExperimentalAnimationApi
+@Navigator.Name("animatedComposable")
+public class AnimatedComposeNavigator : Navigator<AnimatedComposeNavigator.Destination>() {
+    internal val transitionsInProgress get() = state.transitionsInProgress
+
+    internal val backStack get() = state.backStack
+
+    internal val isPop = mutableStateOf(false)
+
+    override fun navigate(
+        entries: List<NavBackStackEntry>,
+        navOptions: NavOptions?,
+        navigatorExtras: Extras?
+    ) {
+        entries.forEach { entry ->
+            state.pushWithTransition(entry)
+        }
+        isPop.value = false
+    }
+
+    override fun createDestination(): Destination {
+        return Destination(this, content = { })
+    }
+
+    override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+        state.popWithTransition(popUpTo, savedState)
+        isPop.value = true
+    }
+
+    internal fun markTransitionComplete(entry: NavBackStackEntry) {
+        state.markTransitionComplete(entry)
+    }
+
+    /**
+     * NavDestination specific to [AnimatedComposeNavigator]
+     */
+    @ExperimentalAnimationApi
+    @NavDestination.ClassType(Composable::class)
+    public class Destination(
+        navigator: AnimatedComposeNavigator,
+        internal val content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
+    ) : NavDestination(navigator)
+
+    internal companion object {
+        internal const val NAME = "animatedComposable"
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt
new file mode 100644
index 0000000..0137572
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.with
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDestination
+import androidx.navigation.NavDestination.Companion.hierarchy
+import androidx.navigation.NavGraph
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import androidx.navigation.Navigator
+import androidx.navigation.compose.DialogHost
+import androidx.navigation.compose.DialogNavigator
+import androidx.navigation.compose.LocalOwnersProvider
+import androidx.navigation.createGraph
+import androidx.navigation.get
+import kotlinx.coroutines.flow.map
+
+/**
+ * Provides in place in the Compose hierarchy for self contained navigation to occur.
+ *
+ * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
+ * the provided [navController].
+ *
+ * The builder passed into this method is [remember]ed. This means that for this NavHost, the
+ * contents of the builder cannot be changed.
+ *
+ * @param navController the navController for this host
+ * @param startDestination the route for the start destination
+ * @param modifier The modifier to be applied to the layout.
+ * @param route the route for the graph
+ * @param enterTransition callback to define enter transitions for destination in this host
+ * @param exitTransition callback to define exit transitions for destination in this host
+ * @param popEnterTransition callback to define popEnter transitions for destination in this host
+ * @param popExitTransition callback to define popExit transitions for destination in this host
+ * @param builder the builder used to construct the graph
+ */
+@Composable
+@ExperimentalAnimationApi
+public fun AnimatedNavHost(
+    navController: NavHostController,
+    startDestination: String,
+    modifier: Modifier = Modifier,
+    contentAlignment: Alignment = Alignment.Center,
+    route: String? = null,
+    enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
+        { fadeIn(animationSpec = tween(700)) },
+    exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) =
+        { fadeOut(animationSpec = tween(700)) },
+    popEnterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
+        enterTransition,
+    popExitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) =
+        exitTransition,
+    builder: NavGraphBuilder.() -> Unit
+) {
+    AnimatedNavHost(
+        navController,
+        remember(route, startDestination, builder) {
+            navController.createGraph(startDestination, route, builder)
+        },
+        modifier,
+        contentAlignment,
+        enterTransition,
+        exitTransition,
+        popEnterTransition,
+        popExitTransition
+    )
+}
+
+/**
+ * Provides in place in the Compose hierarchy for self contained navigation to occur.
+ *
+ * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
+ * the provided [navController].
+ *
+ * @param navController the navController for this host
+ * @param graph the graph for this host
+ * @param modifier The modifier to be applied to the layout.
+ * @param enterTransition callback to define enter transitions for destination in this host
+ * @param exitTransition callback to define exit transitions for destination in this host
+ * @param popEnterTransition callback to define popEnter transitions for destination in this host
+ * @param popExitTransition callback to define popExit transitions for destination in this host
+ */
+@ExperimentalAnimationApi
+@Composable
+public fun AnimatedNavHost(
+    navController: NavHostController,
+    graph: NavGraph,
+    modifier: Modifier = Modifier,
+    contentAlignment: Alignment = Alignment.Center,
+    enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
+        { fadeIn(animationSpec = tween(700)) },
+    exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) =
+        { fadeOut(animationSpec = tween(700)) },
+    popEnterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
+        enterTransition,
+    popExitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) =
+        exitTransition,
+) {
+
+    val lifecycleOwner = LocalLifecycleOwner.current
+    val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
+        "NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
+    }
+    val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+    val onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher
+
+    // on successful recompose we setup the navController with proper inputs
+    // after the first time, this will only happen again if one of the inputs changes
+    navController.setLifecycleOwner(lifecycleOwner)
+    navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
+    if (onBackPressedDispatcher != null) {
+        navController.setOnBackPressedDispatcher(onBackPressedDispatcher)
+    }
+
+    navController.graph = graph
+
+    val saveableStateHolder = rememberSaveableStateHolder()
+
+    // Find the ComposeNavigator, returning early if it isn't found
+    // (such as is the case when using TestNavHostController)
+    val composeNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
+        AnimatedComposeNavigator.NAME
+    ) as? AnimatedComposeNavigator ?: return
+    val visibleEntries by remember(navController.visibleEntries) {
+        navController.visibleEntries.map {
+            it.filter { entry ->
+                entry.destination.navigatorName == AnimatedComposeNavigator.NAME
+            }
+        }
+    }.collectAsState(emptyList())
+
+    val backStackEntry = visibleEntries.lastOrNull()
+
+    if (backStackEntry != null) {
+        val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
+            val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination
+
+            if (composeNavigator.isPop.value) {
+                targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
+                    popEnterTransitions[destination.route]?.invoke(this)
+                } ?: popEnterTransition.invoke(this)
+            } else {
+                targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
+                    enterTransitions[destination.route]?.invoke(this)
+                } ?: enterTransition.invoke(this)
+            }
+        }
+
+        val finalExit: AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition = {
+            val initialDestination =
+                initialState.destination as AnimatedComposeNavigator.Destination
+
+            if (composeNavigator.isPop.value) {
+                initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
+                    popExitTransitions[destination.route]?.invoke(this)
+                } ?: popExitTransition.invoke(this)
+            } else {
+                initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
+                    exitTransitions[destination.route]?.invoke(this)
+                } ?: exitTransition.invoke(this)
+            }
+        }
+
+        val transition = updateTransition(backStackEntry, label = "entry")
+        transition.AnimatedContent(
+            modifier,
+            transitionSpec = {
+                val zIndex = if (composeNavigator.isPop.value) {
+                    visibleEntries.indexOf(initialState).toFloat()
+                } else {
+                    visibleEntries.indexOf(targetState).toFloat()
+                }
+                // If the initialState of the AnimatedContent is not in visibleEntries, we are in
+                // a case where visible has cleared the old state for some reason, so instead of
+                // attempting to animate away from the initialState, we skip the animation.
+                if (initialState in visibleEntries) {
+                    ContentTransform(finalEnter(this), finalExit(this), zIndex)
+                } else {
+                    EnterTransition.None with ExitTransition.None
+                }
+            },
+            contentAlignment,
+            contentKey = { it.id }
+        ) {
+            // In some specific cases, such as clearing your back stack by changing your
+            // start destination, AnimatedContent can contain an entry that is no longer
+            // part of visible entries since it was cleared from the back stack and is not
+            // animating. In these cases the currentEntry will be null, and in those cases,
+            // AnimatedContent will just skip attempting to transition the old entry.
+            // See https://issuetracker.google.com/238686802
+            val currentEntry = visibleEntries.lastOrNull { entry ->
+                it == entry
+            }
+            // while in the scope of the composable, we provide the navBackStackEntry as the
+            // ViewModelStoreOwner and LifecycleOwner
+            currentEntry?.LocalOwnersProvider(saveableStateHolder) {
+                (currentEntry.destination as AnimatedComposeNavigator.Destination)
+                    .content(this, currentEntry)
+            }
+        }
+        if (transition.currentState == transition.targetState) {
+            visibleEntries.forEach { entry ->
+                composeNavigator.markTransitionComplete(entry)
+            }
+        }
+    }
+
+    val dialogNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
+        "dialog"
+    ) as? DialogNavigator ?: return
+
+    // Show any dialog destinations
+    DialogHost(dialogNavigator)
+}
+
+@ExperimentalAnimationApi
+internal val enterTransitions =
+    mutableMapOf<String?,
+        (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?>()
+
+@ExperimentalAnimationApi
+internal val exitTransitions =
+    mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?>()
+
+@ExperimentalAnimationApi
+internal val popEnterTransitions =
+    mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?>()
+
+@ExperimentalAnimationApi
+internal val popExitTransitions =
+    mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?>()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt
new file mode 100644
index 0000000..9e58603
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.Composable
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDeepLink
+import androidx.navigation.NavGraph
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.navigation
+import androidx.navigation.get
+
+/**
+ * Add the [Composable] to the [NavGraphBuilder]
+ *
+ * @param route route for the destination
+ * @param arguments list of arguments to associate with destination
+ * @param deepLinks list of deep links to associate with the destinations
+ * @param enterTransition callback to determine the destination's enter transition
+ * @param exitTransition callback to determine the destination's exit transition
+ * @param popEnterTransition callback to determine the destination's popEnter transition
+ * @param popExitTransition callback to determine the destination's popExit transition
+ * @param content composable for the destination
+ */
+@ExperimentalAnimationApi
+public fun NavGraphBuilder.composable(
+    route: String,
+    arguments: List<NamedNavArgument> = emptyList(),
+    deepLinks: List<NavDeepLink> = emptyList(),
+    enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)? = null,
+    exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)? = null,
+    popEnterTransition: (
+    AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?
+    )? = enterTransition,
+    popExitTransition: (
+    AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?
+    )? = exitTransition,
+    content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
+) {
+    addDestination(
+        AnimatedComposeNavigator.Destination(
+            provider[AnimatedComposeNavigator::class],
+            content
+        ).apply {
+            this.route = route
+            arguments.forEach { (argumentName, argument) ->
+                addArgument(argumentName, argument)
+            }
+            deepLinks.forEach { deepLink ->
+                addDeepLink(deepLink)
+            }
+            enterTransition?.let { enterTransitions[route] = enterTransition }
+            exitTransition?.let { exitTransitions[route] = exitTransition }
+            popEnterTransition?.let { popEnterTransitions[route] = popEnterTransition }
+            popExitTransition?.let { popExitTransitions[route] = popExitTransition }
+        }
+    )
+}
+
+/**
+ * Construct a nested [NavGraph]
+ *
+ * @param startDestination the starting destination's route for this NavGraph
+ * @param route the destination's unique route
+ * @param arguments list of arguments to associate with destination
+ * @param deepLinks list of deep links to associate with the destinations
+ * @param enterTransition callback to define enter transitions for destination in this NavGraph
+ * @param exitTransition callback to define exit transitions for destination in this NavGraph
+ * @param popEnterTransition callback to define pop enter transitions for destination in this
+ * NavGraph
+ * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph
+ * @param builder the builder used to construct the graph
+ *
+ * @return the newly constructed nested NavGraph
+ */
+@ExperimentalAnimationApi
+public fun NavGraphBuilder.navigation(
+    startDestination: String,
+    route: String,
+    arguments: List<NamedNavArgument> = emptyList(),
+    deepLinks: List<NavDeepLink> = emptyList(),
+    enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)? = null,
+    exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)? = null,
+    popEnterTransition: (
+    AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?
+    )? = enterTransition,
+    popExitTransition: (
+    AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?
+    )? = exitTransition,
+    builder: NavGraphBuilder.() -> Unit
+) {
+    navigation(startDestination, route, arguments, deepLinks, builder).apply {
+        enterTransition?.let { enterTransitions[route] = enterTransition }
+        exitTransition?.let { exitTransitions[route] = exitTransition }
+        popEnterTransition?.let { popEnterTransitions[route] = popEnterTransition }
+        popExitTransition?.let { popExitTransitions[route] = popExitTransition }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt
new file mode 100644
index 0000000..a8ac86c
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.navigation.NavDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.Navigator
+import androidx.navigation.compose.rememberNavController
+
+/**
+ * Creates a NavHostController that handles the adding of the [ComposeNavigator], [DialogNavigator]
+ * and [AnimatedComposeNavigator]. Additional [androidx.navigation.Navigator] instances should be
+ * added in a [androidx.compose.runtime.SideEffect] block.
+ *
+ * @see AnimatedNavHost
+ */
+@ExperimentalAnimationApi
+@Composable
+fun rememberAnimatedNavController(
+    vararg navigators: Navigator<out NavDestination>
+): NavHostController {
+    val animatedNavigator = remember { AnimatedComposeNavigator() }
+    return rememberNavController(animatedNavigator, *navigators)
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
index e878804..6cd6e95 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
@@ -17,7 +17,6 @@
 package com.android.settingslib.spaprivileged.model.app
 
 import android.app.AppOpsManager.MODE_ALLOWED
-import android.app.AppOpsManager.MODE_ERRORED
 import android.app.AppOpsManager.Mode
 import android.content.Context
 import android.content.pm.ApplicationInfo
@@ -33,14 +32,14 @@
 
     fun setAllowed(allowed: Boolean)
 
-    @Mode
-    fun getMode(): Int
+    @Mode fun getMode(): Int
 }
 
 class AppOpsController(
     context: Context,
     private val app: ApplicationInfo,
     private val op: Int,
+    private val modeForNotAllowed: Int,
     private val setModeByUid: Boolean = false,
 ) : IAppOpsController {
     private val appOpsManager = context.appOpsManager
@@ -49,7 +48,7 @@
         get() = _mode
 
     override fun setAllowed(allowed: Boolean) {
-        val mode = if (allowed) MODE_ALLOWED else MODE_ERRORED
+        val mode = if (allowed) MODE_ALLOWED else modeForNotAllowed
         if (setModeByUid) {
             appOpsManager.setUidMode(op, app.uid, mode)
         } else {
@@ -58,12 +57,12 @@
         _mode.postValue(mode)
     }
 
-    @Mode
-    override fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName)
+    @Mode override fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName)
 
-    private val _mode = object : MutableLiveData<Int>() {
-        override fun onActive() {
-            postValue(getMode())
+    private val _mode =
+        object : MutableLiveData<Int>() {
+            override fun onActive() {
+                postValue(getMode())
+            }
         }
-    }
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
index ee21b81..53af25b 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
@@ -18,6 +18,7 @@
 
 import android.app.AppOpsManager.MODE_ALLOWED
 import android.app.AppOpsManager.MODE_DEFAULT
+import android.app.AppOpsManager.MODE_ERRORED
 import android.content.Context
 import android.content.pm.ApplicationInfo
 import androidx.compose.runtime.Composable
@@ -25,6 +26,8 @@
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.livedata.observeAsState
 import androidx.compose.runtime.remember
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.framework.util.asyncMapItem
 import com.android.settingslib.spa.framework.util.filterItem
 import com.android.settingslib.spaprivileged.model.app.AppOpsController
 import com.android.settingslib.spaprivileged.model.app.AppRecord
@@ -37,6 +40,7 @@
 
 data class AppOpPermissionRecord(
     override val app: ApplicationInfo,
+    val hasRequestBroaderPermission: Boolean,
     val hasRequestPermission: Boolean,
     var appOpsController: IAppOpsController,
 ) : AppRecord
@@ -50,9 +54,26 @@
     abstract val permission: String
 
     /**
+     * When set, specifies the broader permission who trumps the [permission].
+     *
+     * When trumped, the [permission] is not changeable and model shows the [permission] as allowed.
+     */
+    open val broaderPermission: String? = null
+
+    /**
+     * Indicates whether [permission] has protection level appop flag.
+     *
+     * If true, it uses getAppOpPermissionPackages() to fetch bits to decide whether the permission
+     * is requested.
+     */
+    open val permissionHasAppopFlag: Boolean = true
+
+    open val modeForNotAllowed: Int = MODE_ERRORED
+
+    /**
      * Use AppOpsManager#setUidMode() instead of AppOpsManager#setMode() when set allowed.
      *
-     * Security related app-ops should be set with setUidMode() instead of setMode().
+     * Security or privacy related app-ops should be set with setUidMode() instead of setMode().
      */
     open val setModeByUid = false
 
@@ -60,31 +81,54 @@
     private val notChangeablePackages =
         setOf("android", "com.android.systemui", context.packageName)
 
+    private fun createAppOpsController(app: ApplicationInfo) =
+        AppOpsController(
+            context = context,
+            app = app,
+            op = appOp,
+            setModeByUid = setModeByUid,
+            modeForNotAllowed = modeForNotAllowed,
+        )
+
+    private fun createRecord(
+        app: ApplicationInfo,
+        hasRequestPermission: Boolean
+    ): AppOpPermissionRecord =
+        with(packageManagers) {
+            AppOpPermissionRecord(
+                app = app,
+                hasRequestBroaderPermission =
+                    broaderPermission?.let { app.hasRequestPermission(it) } ?: false,
+                hasRequestPermission = hasRequestPermission,
+                appOpsController = createAppOpsController(app),
+            )
+        }
+
     override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
-        userIdFlow.map { userId ->
-            packageManagers.getAppOpPermissionPackages(userId, permission)
-        }.combine(appListFlow) { packageNames, appList ->
-            appList.map { app ->
-                AppOpPermissionRecord(
-                    app = app,
-                    hasRequestPermission = app.packageName in packageNames,
-                    appOpsController = createAppOpsController(app),
-                )
+        if (permissionHasAppopFlag) {
+            userIdFlow
+                .map { userId -> packageManagers.getAppOpPermissionPackages(userId, permission) }
+                .combine(appListFlow) { packageNames, appList ->
+                    appList.map { app ->
+                        createRecord(
+                            app = app,
+                            hasRequestPermission = app.packageName in packageNames,
+                        )
+                    }
+                }
+        } else {
+            appListFlow.asyncMapItem { app ->
+                with(packageManagers) { createRecord(app, app.hasRequestPermission(permission)) }
             }
         }
 
-    override fun transformItem(app: ApplicationInfo) = AppOpPermissionRecord(
-        app = app,
-        hasRequestPermission = with(packageManagers) { app.hasRequestPermission(permission) },
-        appOpsController = createAppOpsController(app),
-    )
-
-    private fun createAppOpsController(app: ApplicationInfo) = AppOpsController(
-        context = context,
-        app = app,
-        op = appOp,
-        setModeByUid = setModeByUid,
-    )
+    override fun transformItem(app: ApplicationInfo) =
+        with(packageManagers) {
+            createRecord(
+                app = app,
+                hasRequestPermission = app.hasRequestPermission(permission),
+            )
+        }
 
     override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) =
         recordListFlow.filterItem(::isChangeable)
@@ -95,15 +139,19 @@
      */
     @Composable
     override fun isAllowed(record: AppOpPermissionRecord): State<Boolean?> {
+        if (record.hasRequestBroaderPermission) {
+            // Broader permission trumps the specific permission.
+            return stateOf(true)
+        }
+
         val mode = record.appOpsController.mode.observeAsState()
         return remember {
             derivedStateOf {
                 when (mode.value) {
                     null -> null
                     MODE_ALLOWED -> true
-                    MODE_DEFAULT -> with(packageManagers) {
-                        record.app.hasGrantPermission(permission)
-                    }
+                    MODE_DEFAULT ->
+                        with(packageManagers) { record.app.hasGrantPermission(permission) }
                     else -> false
                 }
             }
@@ -111,7 +159,9 @@
     }
 
     override fun isChangeable(record: AppOpPermissionRecord) =
-        record.hasRequestPermission && record.app.packageName !in notChangeablePackages
+        record.hasRequestPermission &&
+            !record.hasRequestBroaderPermission &&
+            record.app.packageName !in notChangeablePackages
 
     override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
         record.appOpsController.setAllowed(newAllowed)
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 76cff0b..e9fcbd2 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
@@ -81,6 +81,13 @@
             navArgument(USER_ID) { type = NavType.IntType },
         )
 
+        /**
+         * Gets the route prefix to this page.
+         *
+         * Expose route prefix to enable enter from non-SPA pages.
+         */
+        fun getRoutePrefix(permissionType: String) = "$PAGE_NAME/$permissionType"
+
         @Composable
         fun navigator(permissionType: String, app: ApplicationInfo) =
             navigator(route = "$PAGE_NAME/$permissionType/${app.toRoute()}")
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
index ce8fc9d..1ab6230 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
@@ -93,6 +93,14 @@
     fun getAppListRoute(): String =
         TogglePermissionAppListPageProvider.getRoute(permissionType)
 
+    /**
+     * Gets the route prefix to the toggle permission App Info page.
+     *
+     * Expose route prefix to enable enter from non-SPA pages.
+     */
+    fun getAppInfoRoutePrefix(): String =
+        TogglePermissionAppInfoPageProvider.getRoutePrefix(permissionType)
+
     @Composable
     fun InfoPageEntryItem(app: ApplicationInfo) {
         val listModel = rememberContext(::createModel)
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt
index 668bfdf..53e52d0 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt
@@ -31,21 +31,18 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.Spy
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
-import org.mockito.Mockito.`when` as whenever
 
 @RunWith(AndroidJUnit4::class)
 class AppOpsControllerTest {
-    @get:Rule
-    val mockito: MockitoRule = MockitoJUnit.rule()
+    @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
 
-    @Spy
-    private val context: Context = ApplicationProvider.getApplicationContext()
+    @Spy private val context: Context = ApplicationProvider.getApplicationContext()
 
-    @Mock
-    private lateinit var appOpsManager: AppOpsManager
+    @Mock private lateinit var appOpsManager: AppOpsManager
 
     @Before
     fun setUp() {
@@ -54,7 +51,13 @@
 
     @Test
     fun setAllowed_setToTrue() {
-        val controller = AppOpsController(context = context, app = APP, op = OP)
+        val controller =
+            AppOpsController(
+                context = context,
+                app = APP,
+                op = OP,
+                modeForNotAllowed = MODE_ERRORED
+            )
 
         controller.setAllowed(true)
 
@@ -63,7 +66,13 @@
 
     @Test
     fun setAllowed_setToFalse() {
-        val controller = AppOpsController(context = context, app = APP, op = OP)
+        val controller =
+            AppOpsController(
+                context = context,
+                app = APP,
+                op = OP,
+                modeForNotAllowed = MODE_ERRORED
+            )
 
         controller.setAllowed(false)
 
@@ -73,7 +82,13 @@
     @Test
     fun setAllowed_setToTrueByUid() {
         val controller =
-            AppOpsController(context = context, app = APP, op = OP, setModeByUid = true)
+            AppOpsController(
+                context = context,
+                app = APP,
+                op = OP,
+                modeForNotAllowed = MODE_ERRORED,
+                setModeByUid = true
+            )
 
         controller.setAllowed(true)
 
@@ -83,7 +98,13 @@
     @Test
     fun setAllowed_setToFalseByUid() {
         val controller =
-            AppOpsController(context = context, app = APP, op = OP, setModeByUid = true)
+            AppOpsController(
+                context = context,
+                app = APP,
+                op = OP,
+                modeForNotAllowed = MODE_ERRORED,
+                setModeByUid = true
+            )
 
         controller.setAllowed(false)
 
@@ -92,10 +113,15 @@
 
     @Test
     fun getMode() {
-        whenever(
-            appOpsManager.checkOpNoThrow(OP, APP.uid, APP.packageName)
-        ).thenReturn(MODE_ALLOWED)
-        val controller = AppOpsController(context = context, app = APP, op = OP)
+        whenever(appOpsManager.checkOpNoThrow(OP, APP.uid, APP.packageName))
+            .thenReturn(MODE_ALLOWED)
+        val controller =
+            AppOpsController(
+                context = context,
+                app = APP,
+                op = OP,
+                modeForNotAllowed = MODE_ERRORED
+            )
 
         val mode = controller.getMode()
 
@@ -104,9 +130,10 @@
 
     private companion object {
         const val OP = 1
-        val APP = ApplicationInfo().apply {
-            packageName = "package.name"
-            uid = 123
-        }
+        val APP =
+            ApplicationInfo().apply {
+                packageName = "package.name"
+                uid = 123
+            }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
index 966b869..da765ba 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
@@ -39,28 +39,23 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.Spy
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
-import org.mockito.Mockito.`when` as whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 class AppOpPermissionAppListTest {
-    @get:Rule
-    val mockito: MockitoRule = MockitoJUnit.rule()
+    @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
 
-    @get:Rule
-    val composeTestRule = createComposeRule()
+    @get:Rule val composeTestRule = createComposeRule()
 
-    @Spy
-    private val context: Context = ApplicationProvider.getApplicationContext()
+    @Spy private val context: Context = ApplicationProvider.getApplicationContext()
 
-    @Mock
-    private lateinit var packageManagers: IPackageManagers
+    @Mock private lateinit var packageManagers: IPackageManagers
 
-    @Mock
-    private lateinit var appOpsManager: AppOpsManager
+    @Mock private lateinit var appOpsManager: AppOpsManager
 
     private lateinit var listModel: TestAppOpPermissionAppListModel
 
@@ -79,9 +74,7 @@
 
     @Test
     fun transformItem_hasRequestPermission() = runTest {
-        with(packageManagers) {
-            whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(true)
-        }
+        with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(true) }
 
         val record = listModel.transformItem(APP)
 
@@ -90,8 +83,30 @@
 
     @Test
     fun transformItem_notRequestPermission() = runTest {
+        with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false) }
+
+        val record = listModel.transformItem(APP)
+
+        assertThat(record.hasRequestPermission).isFalse()
+    }
+
+    @Test
+    fun transformItem_hasRequestBroaderPermission() = runTest {
+        listModel.broaderPermission = BROADER_PERMISSION
         with(packageManagers) {
-            whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false)
+            whenever(APP.hasRequestPermission(BROADER_PERMISSION)).thenReturn(true)
+        }
+
+        val record = listModel.transformItem(APP)
+
+        assertThat(record.hasRequestBroaderPermission).isTrue()
+    }
+
+    @Test
+    fun transformItem_notRequestBroaderPermission() = runTest {
+        listModel.broaderPermission = BROADER_PERMISSION
+        with(packageManagers) {
+            whenever(APP.hasRequestPermission(BROADER_PERMISSION)).thenReturn(false)
         }
 
         val record = listModel.transformItem(APP)
@@ -101,14 +116,14 @@
 
     @Test
     fun filter() = runTest {
-        with(packageManagers) {
-            whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false)
-        }
-        val record = AppOpPermissionRecord(
-            app = APP,
-            hasRequestPermission = false,
-            appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
-        )
+        with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false) }
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = false,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+            )
 
         val recordListFlow = listModel.filter(flowOf(USER_ID), flowOf(listOf(record)))
 
@@ -118,11 +133,13 @@
 
     @Test
     fun isAllowed_allowed() {
-        val record = AppOpPermissionRecord(
-            app = APP,
-            hasRequestPermission = true,
-            appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED),
-        )
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = true,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED),
+            )
 
         val isAllowed = getIsAllowed(record)
 
@@ -131,14 +148,14 @@
 
     @Test
     fun isAllowed_defaultAndHasGrantPermission() {
-        with(packageManagers) {
-            whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(true)
-        }
-        val record = AppOpPermissionRecord(
-            app = APP,
-            hasRequestPermission = true,
-            appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
-        )
+        with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(true) }
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = true,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+            )
 
         val isAllowed = getIsAllowed(record)
 
@@ -147,14 +164,14 @@
 
     @Test
     fun isAllowed_defaultAndNotGrantPermission() {
-        with(packageManagers) {
-            whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false)
-        }
-        val record = AppOpPermissionRecord(
-            app = APP,
-            hasRequestPermission = true,
-            appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
-        )
+        with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false) }
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = true,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+            )
 
         val isAllowed = getIsAllowed(record)
 
@@ -162,12 +179,34 @@
     }
 
     @Test
+    fun isAllowed_broaderPermissionTrumps() {
+        listModel.broaderPermission = BROADER_PERMISSION
+        with(packageManagers) {
+            whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false)
+            whenever(APP.hasGrantPermission(BROADER_PERMISSION)).thenReturn(true)
+        }
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = true,
+                hasRequestPermission = false,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED),
+            )
+
+        val isAllowed = getIsAllowed(record)
+
+        assertThat(isAllowed).isTrue()
+    }
+
+    @Test
     fun isAllowed_notAllowed() {
-        val record = AppOpPermissionRecord(
-            app = APP,
-            hasRequestPermission = true,
-            appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED),
-        )
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = true,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED),
+            )
 
         val isAllowed = getIsAllowed(record)
 
@@ -176,11 +215,13 @@
 
     @Test
     fun isChangeable_notRequestPermission() {
-        val record = AppOpPermissionRecord(
-            app = APP,
-            hasRequestPermission = false,
-            appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
-        )
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = false,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+            )
 
         val isChangeable = listModel.isChangeable(record)
 
@@ -189,11 +230,13 @@
 
     @Test
     fun isChangeable_notChangeablePackages() {
-        val record = AppOpPermissionRecord(
-            app = NOT_CHANGEABLE_APP,
-            hasRequestPermission = true,
-            appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
-        )
+        val record =
+            AppOpPermissionRecord(
+                app = NOT_CHANGEABLE_APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = true,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+            )
 
         val isChangeable = listModel.isChangeable(record)
 
@@ -202,11 +245,13 @@
 
     @Test
     fun isChangeable_hasRequestPermissionAndChangeable() {
-        val record = AppOpPermissionRecord(
-            app = APP,
-            hasRequestPermission = true,
-            appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
-        )
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = true,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+            )
 
         val isChangeable = listModel.isChangeable(record)
 
@@ -214,13 +259,31 @@
     }
 
     @Test
+    fun isChangeable_broaderPermissionTrumps() {
+        listModel.broaderPermission = BROADER_PERMISSION
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = true,
+                hasRequestPermission = true,
+                appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+            )
+
+        val isChangeable = listModel.isChangeable(record)
+
+        assertThat(isChangeable).isFalse()
+    }
+
+    @Test
     fun setAllowed() {
         val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
-        val record = AppOpPermissionRecord(
-            app = APP,
-            hasRequestPermission = true,
-            appOpsController = appOpsController,
-        )
+        val record =
+            AppOpPermissionRecord(
+                app = APP,
+                hasRequestBroaderPermission = false,
+                hasRequestPermission = true,
+                appOpsController = appOpsController,
+            )
 
         listModel.setAllowed(record = record, newAllowed = true)
 
@@ -239,9 +302,7 @@
 
     private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? {
         lateinit var isAllowedState: State<Boolean?>
-        composeTestRule.setContent {
-            isAllowedState = listModel.isAllowed(record)
-        }
+        composeTestRule.setContent { isAllowedState = listModel.isAllowed(record) }
         return isAllowedState.value
     }
 
@@ -250,8 +311,12 @@
         override val pageTitleResId = R.string.test_app_op_permission_title
         override val switchTitleResId = R.string.test_app_op_permission_switch_title
         override val footerResId = R.string.test_app_op_permission_footer
+
         override val appOp = AppOpsManager.OP_MANAGE_MEDIA
         override val permission = PERMISSION
+        override val permissionHasAppopFlag = true
+        override var broaderPermission: String? = null
+
         override var setModeByUid = false
     }
 
@@ -259,12 +324,9 @@
         const val USER_ID = 0
         const val PACKAGE_NAME = "package.name"
         const val PERMISSION = "PERMISSION"
-        val APP = ApplicationInfo().apply {
-            packageName = PACKAGE_NAME
-        }
-        val NOT_CHANGEABLE_APP = ApplicationInfo().apply {
-            packageName = "android"
-        }
+        const val BROADER_PERMISSION = "BROADER_PERMISSION"
+        val APP = ApplicationInfo().apply { packageName = PACKAGE_NAME }
+        val NOT_CHANGEABLE_APP = ApplicationInfo().apply { packageName = "android" }
     }
 }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 4a1a4e6..810545c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -291,6 +291,18 @@
         return false;
     }
 
+    /**
+     * Check if a device class matches with a defined BluetoothClass device.
+     *
+     * @param device Must be one of the public constants in {@link BluetoothClass.Device}
+     * @return true if device class matches, false otherwise.
+     */
+    public static boolean isDeviceClassMatched(@NonNull BluetoothDevice bluetoothDevice,
+            int device) {
+        return bluetoothDevice.getBluetoothClass() != null
+                && bluetoothDevice.getBluetoothClass().getDeviceClass() == device;
+    }
+
     private static boolean isAdvancedHeaderEnabled() {
         if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED,
                 true)) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 2951001..c2c1b55 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1028,12 +1028,11 @@
         if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
             // The pairing dialog now warns of phone-book access for paired devices.
             // No separate prompt is displayed after pairing.
-            final BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
             if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
-                if (bluetoothClass != null && (bluetoothClass.getDeviceClass()
-                        == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE
-                        || bluetoothClass.getDeviceClass()
-                        == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)) {
+                if (BluetoothUtils.isDeviceClassMatched(mDevice,
+                        BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE)
+                        || BluetoothUtils.isDeviceClassMatched(mDevice,
+                        BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)) {
                     EventLog.writeEvent(0x534e4554, "138529441", -1, "");
                 }
             }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 221836b..f741f65 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -390,7 +390,10 @@
         Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " by CSIP");
         mOngoingSetMemberPair = device;
         syncConfigFromMainDevice(device, groupId);
-        device.createBond(BluetoothDevice.TRANSPORT_LE);
+        if (!device.createBond(BluetoothDevice.TRANSPORT_LE)) {
+            Log.d(TAG, "Bonding could not be started");
+            mOngoingSetMemberPair = null;
+        }
     }
 
     private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java
index feb5e0b..1401a4f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java
@@ -41,8 +41,8 @@
     }
 
     /**
-     * Logs hearing aid device information to westworld, including device mode, device side, and
-     * entry page id where the binding(connecting) process starts.
+     * Logs hearing aid device information to statsd, including device mode, device side, and entry
+     * page id where the binding(connecting) process starts.
      *
      * Only logs the info once after hearing aid is bonded(connected). Clears the map entry of this
      * device when logging is completed.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
index 562d480..e781f13 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -275,6 +275,10 @@
     }
 
     public int getDrawableResource(BluetoothClass btClass) {
+        if (btClass == null) {
+            Log.e(TAG, "No btClass.");
+            return R.drawable.ic_bt_le_audio_speakers;
+        }
         switch (btClass.getDeviceClass()) {
             case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED:
             case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index f92625b..b22195a 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -32,18 +32,19 @@
         }
       ]
     },
-    {
-      // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+)
-      "name": "SystemUIGoogleBiometricsScreenshotTests",
-      "options": [
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
-    },
+// Disable until can pass: b/259124654
+//    {
+//      // 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",
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
new file mode 100644
index 0000000..a494f5e
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2023 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "AccessibilityMenu",
+    srcs: [
+        "src/**/*.java",
+    ],
+    system_ext_specific: true,
+    platform_apis: true,
+    resource_dirs: ["res"],
+    certificate: "platform",
+    // This app uses allowlisted privileged permissions.
+    privileged: true,
+}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
new file mode 100644
index 0000000..26748a9
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.android.systemui.accessibility.accessibilitymenu">
+    <application>
+        <service
+            android:name="com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService"
+            android:exported="false"
+            android:label="Accessibility Menu (System)"
+            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+            <intent-filter>
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+            </intent-filter>
+            <meta-data
+                android:name="android.accessibilityservice"
+                android:resource="@xml/accessibilitymenu_service"/>
+        </service>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/OWNERS b/packages/SystemUI/accessibility/accessibilitymenu/OWNERS
new file mode 100644
index 0000000..b74281e
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/view/accessibility/OWNERS
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml
new file mode 100644
index 0000000..96882d33
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"/>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
new file mode 100644
index 0000000..8b75900
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 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.accessibility.accessibilitymenu;
+
+import android.accessibilityservice.AccessibilityService;
+import android.view.accessibility.AccessibilityEvent;
+
+/** @hide */
+public class AccessibilityMenuService extends AccessibilityService {
+
+    @Override
+    public void onAccessibilityEvent(AccessibilityEvent event) {
+    }
+
+    @Override
+    public void onInterrupt() {
+    }
+}
diff --git a/packages/SystemUI/docs/modern-architecture.png b/packages/SystemUI/docs/modern-architecture.png
new file mode 100644
index 0000000..2636362
--- /dev/null
+++ b/packages/SystemUI/docs/modern-architecture.png
Binary files differ
diff --git a/packages/SystemUI/docs/status-bar-data-pipeline.md b/packages/SystemUI/docs/status-bar-data-pipeline.md
new file mode 100644
index 0000000..9fcdce1
--- /dev/null
+++ b/packages/SystemUI/docs/status-bar-data-pipeline.md
@@ -0,0 +1,261 @@
+# Status Bar Data Pipeline
+
+## Background
+
+The status bar is the UI shown at the top of the user's screen that gives them
+information about the time, notifications, and system status like mobile
+conectivity and battery level. This document is about the implementation of the
+wifi and mobile system icons on the right side:
+
+![image of status bar](status-bar.png)
+
+In Android U, the data pipeline that determines what mobile and wifi icons to
+show in the status bar has been re-written with a new architecture. This format
+generally follows Android best practices to
+[app architecture](https://developer.android.com/topic/architecture#recommended-app-arch).
+This document serves as a guide for the new architecture, and as a guide for how
+OEMs can add customizations to the new architecture.
+
+## Architecture
+
+In the new architecture, there is a separate pipeline for each type of icon. For
+Android U, **only the wifi icon and mobile icons have been implemented in the
+new architecture**.
+
+As shown in the Android best practices guide, each new pipeline has a data
+layer, a domain layer, and a UI layer:
+
+![diagram of UI, domain, and data layers](modern-architecture.png)
+
+The classes in the data layer are `repository` instances. The classes in the
+domain layer are `interactor` instances. The classes in the UI layer are
+`viewmodel` instances and `viewbinder` instances. In this document, "repository"
+and "data layer" will be used interchangably (and the same goes for the other
+layers).
+
+The wifi logic is in `statusbar/pipeline/wifi` and the mobile logic is in
+`statusbar/pipeline/mobile`.
+
+#### Repository (data layer)
+
+System callbacks, broadcast receivers, configuration values are all defined
+here, and exposed through the appropriate interface. Where appropriate, we
+define `Model` objects at this layer so that clients do not have to rely on
+system-defined interfaces.
+
+#### Interactor (domain layer)
+
+Here is where we define the business logic and transform the data layer objects
+into something consumable by the ViewModel classes. For example,
+`MobileIconsInteractor` defines the CBRS filtering logic by exposing a
+`filteredSubscriptions` list.
+
+#### ViewModel (UI layer)
+
+View models should define the final piece of business logic mapping to UI logic.
+For example, the mobile view model checks the `IconInteractor.isRoaming` flow to
+decide whether or not to show the roaming indicator.
+
+#### ViewBinder
+
+These have already been implemented and configured. ViewBinders replace the old
+`applyMobileState` mechanism that existed in the `IconManager` classes of the
+old pipeline. A view binder associates a ViewModel with a View, and keeps the
+view up-to-date with the most recent information from the model.
+
+Any new fields added to the ViewModel classes need to be equivalently bound to
+the view here.
+
+### Putting it all together
+
+Putting that altogether, we have this overall architecture diagram for the
+icons:
+
+![diagram of wifi and mobile pipelines](status-bar-pipeline.png)
+
+### Mobile icons architecture
+
+Because there can be multiple mobile connections at the same time, the mobile
+pipeline is split up hierarchically. At each level (data, domain, and UI), there
+is a singleton parent class that manages information relevant to **all** mobile
+connections, and multiple instances of child classes that manage information for
+a **single** mobile connection.
+
+For example, `MobileConnectionsRepository` is a singleton at the data layer that
+stores information relevant to **all** mobile connections, and it also manages a
+list of child `MobileConnectionRepository` classes. `MobileConnectionRepository`
+is **not** a singleton, and each individual `MobileConnectionRepository`
+instance fully qualifies the state of a **single** connection. This pattern is
+repeated at the `Interactor` and `ViewModel` layers for mobile.
+
+![diagram of mobile parent child relationship](status-bar-mobile-pipeline.png)
+
+Note: Since there is at most one wifi connection, the wifi pipeline is not split
+up in the same way.
+
+## Customizations
+
+The new pipeline completely replaces these classes:
+
+*   `WifiStatusTracker`
+*   `MobileStatusTracker`
+*   `NetworkSignalController` and `NetworkSignalControllerImpl`
+*   `MobileSignalController`
+*   `WifiSignalController`
+*   `StatusBarSignalPolicy` (including `SignalIconState`, `MobileIconState`, and
+    `WifiIconState`)
+
+Any customizations in any of these classes will need to be migrated to the new
+pipeline. As a general rule, any change that would have gone into
+`NetworkControllerImpl` would be done in `MobileConnectionsRepository`, and any
+change for `MobileSignalController` can be done in `MobileConnectionRepository`
+(see above on the relationship between those repositories).
+
+### Sample customization: New service
+
+Some customizations require listening to additional services to get additional
+data. This new architecture makes it easy to add additional services to the
+status bar data pipeline to get icon customizations.
+
+Below is a general guide to how a new service should be added. However, there
+may be exceptions to this guide for specific use cases.
+
+1.  In the data layer (`repository` classes), add a new `StateFlow` that listens
+    to the service:
+
+    ```kotlin
+    class MobileConnectionsRepositoryImpl {
+      ...
+      val fooVal: StateFlow<Int> =
+        conflatedCallbackFlow {
+          val callback = object : FooServiceCallback(), FooListener {
+            override fun onFooChanged(foo: Int) {
+              trySend(foo)
+            }
+          }
+
+          fooService.registerCallback(callback)
+
+          awaitClose { fooService.unregisterCallback(callback) }
+        }
+          .stateIn(scope, started = SharingStarted.WhileSubscribed(), FOO_DEFAULT_VAL)
+    }
+    ```
+
+1.  In the domain layer (`interactor` classes), either use this new flow to
+    process values, or just expose the flow as-is for the UI layer.
+
+    For example, if `bar` should only be true when `foo` is positive:
+
+    ```kotlin
+    class MobileIconsInteractor {
+      ...
+      val bar: StateFlow<Boolean> =
+        mobileConnectionsRepo
+          .mapLatest { foo -> foo > 0 }
+          .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = false)
+    }
+    ```
+
+1.  In the UI layer (`viewmodel` classes), update the existing flows to process
+    the new value from the interactor.
+
+    For example, if the icon should be hidden when `bar` is true:
+
+    ```kotlin
+    class MobileIconViewModel {
+      ...
+      iconId: Flow<Int> = combine(
+        iconInteractor.level,
+        iconInteractor.numberOfLevels,
+        iconInteractor.bar,
+    ) { level, numberOfLevels, bar ->
+      if (bar) {
+        null
+      } else {
+        calcIcon(level, numberOfLevels)
+      }
+    }
+    ```
+
+## Demo mode
+
+SystemUI demo mode is a first-class citizen in the new pipeline. It is
+implemented as an entirely separate repository,
+`DemoMobileConnectionsRepository`. When the system moves into demo mode, the
+implementation of the data layer is switched to the demo repository via the
+`MobileRepositorySwitcher` class.
+
+Because the demo mode repositories implement the same interfaces as the
+production classes, any changes made above will have to be implemented for demo
+mode as well.
+
+1.  Following from above, if `fooVal` is added to the
+    `MobileConnectionsRepository` interface:
+
+    ```kotlin
+    class DemoMobileConnectionsRepository {
+      private val _fooVal = MutableStateFlow(FOO_DEFAULT_VALUE)
+      override val fooVal: StateFlow<Int> = _fooVal.asStateFlow()
+
+      // Process the state. **See below on how to add the command to the CLI**
+      fun processEnabledMobileState(state: Mobile) {
+        ...
+        _fooVal.value = state.fooVal
+      }
+    }
+    ```
+
+1.  (Optional) If you want to enable the command line interface for setting and
+    testing this value in demo mode, you can add parsing logic to
+    `DemoModeMobileConnectionDataSource` and `FakeNetworkEventModel`:
+
+    ```kotlin
+    sealed interface FakeNetworkEventModel {
+      data class Mobile(
+      ...
+      // Add new fields here
+      val fooVal: Int?
+      )
+    }
+    ```
+
+    ```kotlin
+    class DemoModeMobileConnectionDataSource {
+      // Currently, the demo commands are implemented as an extension function on Bundle
+      private fun Bundle.activeMobileEvent(): Mobile {
+        ...
+        val fooVal = getString("fooVal")?.toInt()
+        return Mobile(
+          ...
+          fooVal = fooVal,
+        )
+      }
+    }
+    ```
+
+If step 2 is implemented, then you will be able to pass demo commands via the
+command line:
+
+```
+adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e fooVal <test value>
+```
+
+## Migration plan
+
+For Android U, the new pipeline will be enabled and default. However, the old
+pipeline code will still be around just in case the new pipeline doesn’t do well
+in the testing phase.
+
+For Android V, the old pipeline will be completely removed and the new pipeline
+will be the one source of truth.
+
+Our ask for OEMs is to default to using the new pipeline in Android U. If there
+are customizations that seem difficult to migrate over to the new pipeline,
+please file a bug with us and we’d be more than happy to consult on the best
+solution. The new pipeline was designed with customizability in mind, so our
+hope is that working the new pipeline will be easier and faster.
+
+Note: The new pipeline currently only supports the wifi and mobile icons. The
+other system status bar icons may be migrated to a similar architecture in the
+future.
diff --git a/packages/SystemUI/docs/status-bar-mobile-pipeline.png b/packages/SystemUI/docs/status-bar-mobile-pipeline.png
new file mode 100644
index 0000000..620563d
--- /dev/null
+++ b/packages/SystemUI/docs/status-bar-mobile-pipeline.png
Binary files differ
diff --git a/packages/SystemUI/docs/status-bar-pipeline.png b/packages/SystemUI/docs/status-bar-pipeline.png
new file mode 100644
index 0000000..1c568c9
--- /dev/null
+++ b/packages/SystemUI/docs/status-bar-pipeline.png
Binary files differ
diff --git a/packages/SystemUI/docs/status-bar.png b/packages/SystemUI/docs/status-bar.png
new file mode 100644
index 0000000..3a5af0e
--- /dev/null
+++ b/packages/SystemUI/docs/status-bar.png
Binary files differ
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 7243ca4..d38efcd 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -460,7 +460,6 @@
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 1ef523b..7073f6a 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -661,7 +661,9 @@
         <item>7</item> <!-- WAKE_REASON_WAKE_MOTION -->
         <item>9</item> <!-- WAKE_REASON_LID -->
         <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED -->
-        <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE -->
+        <item>15</item> <!-- WAKE_REASON_TAP -->
+        <item>16</item> <!-- WAKE_REASON_LIFT -->
+        <item>17</item> <!-- WAKE_REASON_BIOMETRIC -->
     </integer-array>
 
     <!-- Whether the communal service should be enabled -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 07e8bad..d6ccdfe 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2301,13 +2301,13 @@
     <string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string>
     <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]-->
     <string name="accessibility_floating_button_undo">Undo</string>
-
+    <!-- Text for the message view with undo action of the accessibility floating menu to show which feature shortcut was removed. [CHAR LIMIT=30]-->
+    <string name="accessibility_floating_button_undo_message_label_text"><xliff:g id="feature name" example="Magnification">%s</xliff:g> shortcut removed</string>
     <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]-->
-    <string name="accessibility_floating_button_undo_message_text">{count, plural,
-        =1 {{label} shortcut removed}
+    <string name="accessibility_floating_button_undo_message_number_text">{count, plural,
+        =1 {# shortcut removed}
         other {# shortcuts removed}
     }</string>
-
     <!-- Action in accessibility menu to move the accessibility floating button to the top left of the screen. [CHAR LIMIT=30] -->
     <string name="accessibility_floating_button_action_move_top_left">Move top left</string>
     <!-- Action in accessibility menu to move the accessibility floating button to the top right of the screen. [CHAR LIMIT=30] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 061ca4f..67e3400 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -177,6 +177,8 @@
 
     @Override
     public void startAppearAnimation() {
+        setAlpha(1f);
+        setTranslationY(0);
         if (mAppearAnimator.isRunning()) {
             mAppearAnimator.cancel();
         }
@@ -213,7 +215,6 @@
 
     /** Animate subviews according to expansion or time. */
     private void animate(float progress) {
-        setAlpha(progress);
         Interpolator standardDecelerate = Interpolators.STANDARD_DECELERATE;
         Interpolator legacyDecelerate = Interpolators.LEGACY_DECELERATE;
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 777d10c..6d54d38 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -182,9 +182,9 @@
         if (mFloatingMenu == null) {
             if (mFeatureFlags.isEnabled(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS)) {
                 final Display defaultDisplay = mDisplayManager.getDisplay(DEFAULT_DISPLAY);
-                mFloatingMenu = new MenuViewLayerController(
-                        mContext.createWindowContext(defaultDisplay,
-                                TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager,
+                final Context windowContext = mContext.createWindowContext(defaultDisplay,
+                        TYPE_NAVIGATION_BAR_PANEL, /* options= */ null);
+                mFloatingMenu = new MenuViewLayerController(windowContext, mWindowManager,
                         mAccessibilityManager);
             } else {
                 mFloatingMenu = new AccessibilityFloatingMenu(mContext);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
index ee048e1..c2bc140 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
@@ -19,8 +19,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
-import android.content.ComponentCallbacks;
-import android.content.res.Configuration;
 import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
@@ -34,7 +32,7 @@
  * Controls the interaction between {@link MagnetizedObject} and
  * {@link MagnetizedObject.MagneticTarget}.
  */
-class DismissAnimationController implements ComponentCallbacks {
+class DismissAnimationController {
     private static final float COMPLETELY_OPAQUE = 1.0f;
     private static final float COMPLETELY_TRANSPARENT = 0.0f;
     private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f;
@@ -105,16 +103,6 @@
         mMagnetizedObject.addTarget(magneticTarget);
     }
 
-    @Override
-    public void onConfigurationChanged(@NonNull Configuration newConfig) {
-        updateResources();
-    }
-
-    @Override
-    public void onLowMemory() {
-        // Do nothing
-    }
-
     void showDismissView(boolean show) {
         if (show) {
             mDismissView.show();
@@ -165,7 +153,7 @@
         }
     }
 
-    private void updateResources() {
+    void updateResources() {
         final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize(
                 R.dimen.dismiss_circle_size);
         mMinDismissSize = mDismissView.getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipView.java
index 4400534..5ec024e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipView.java
@@ -21,6 +21,7 @@
 import static android.view.View.MeasureSpec.UNSPECIFIED;
 
 import android.annotation.SuppressLint;
+import android.content.ComponentCallbacks;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -48,7 +49,7 @@
  * . It's just shown on the left or right of the anchor view.
  */
 @SuppressLint("ViewConstructor")
-class MenuEduTooltipView extends FrameLayout {
+class MenuEduTooltipView extends FrameLayout implements ComponentCallbacks {
     private int mFontSize;
     private int mTextViewMargin;
     private int mTextViewPadding;
@@ -73,9 +74,7 @@
     }
 
     @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
         updateResources();
         updateMessageView();
         updateArrowView();
@@ -83,6 +82,25 @@
         updateLocationAndVisibility();
     }
 
+    @Override
+    public void onLowMemory() {
+        // Do nothing.
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        getContext().registerComponentCallbacks(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        getContext().unregisterComponentCallbacks(this);
+    }
+
     void show(CharSequence message) {
         mMessageView.setText(message);
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
index 05e1d3f..f79c3d2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
@@ -30,13 +30,21 @@
 
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
+import android.content.ComponentCallbacks;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.database.ContentObserver;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
+import android.util.Log;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.NonNull;
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.annotations.VisibleForTesting;
@@ -50,6 +58,9 @@
  * Stores and observe the settings contents for the menu view.
  */
 class MenuInfoRepository {
+    private static final String TAG = "MenuInfoRepository";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE;
+
     @FloatRange(from = 0.0, to = 1.0)
     private static final float DEFAULT_MENU_POSITION_X_PERCENT = 1.0f;
 
@@ -60,6 +71,10 @@
     private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED;
 
     private final Context mContext;
+    private final Configuration mConfiguration;
+    private final AccessibilityManager mAccessibilityManager;
+    private final AccessibilityManager.AccessibilityServicesStateChangeListener
+            mA11yServicesStateChangeListener = manager -> onTargetFeaturesChanged();
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final OnSettingsContentsChanged mSettingsContentsCallback;
     private Position mPercentagePosition;
@@ -74,12 +89,12 @@
         int ENABLED = 1;
     }
 
-    private final ContentObserver mMenuTargetFeaturesContentObserver =
+    @VisibleForTesting
+    final ContentObserver mMenuTargetFeaturesContentObserver =
             new ContentObserver(mHandler) {
                 @Override
                 public void onChange(boolean selfChange) {
-                    mSettingsContentsCallback.onTargetFeaturesChanged(
-                            getTargets(mContext, ACCESSIBILITY_BUTTON));
+                    onTargetFeaturesChanged();
                 }
             };
 
@@ -102,8 +117,35 @@
                 }
             };
 
-    MenuInfoRepository(Context context, OnSettingsContentsChanged settingsContentsChanged) {
+    @VisibleForTesting
+    final ComponentCallbacks mComponentCallbacks = new ComponentCallbacks() {
+        @Override
+        public void onConfigurationChanged(@NonNull Configuration newConfig) {
+            final int diff = newConfig.diff(mConfiguration);
+
+            if (DEBUG) {
+                Log.d(TAG, "onConfigurationChanged = " + Configuration.configurationDiffToString(
+                        diff));
+            }
+
+            if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) {
+                onTargetFeaturesChanged();
+            }
+
+            mConfiguration.setTo(newConfig);
+        }
+
+        @Override
+        public void onLowMemory() {
+            // Do nothing.
+        }
+    };
+
+    MenuInfoRepository(Context context, AccessibilityManager accessibilityManager,
+            OnSettingsContentsChanged settingsContentsChanged) {
         mContext = context;
+        mAccessibilityManager = accessibilityManager;
+        mConfiguration = new Configuration(context.getResources().getConfiguration());
         mSettingsContentsCallback = settingsContentsChanged;
 
         mPercentagePosition = getStartPosition();
@@ -172,6 +214,11 @@
                 UserHandle.USER_CURRENT);
     }
 
+    private void onTargetFeaturesChanged() {
+        mSettingsContentsCallback.onTargetFeaturesChanged(
+                getTargets(mContext, ACCESSIBILITY_BUTTON));
+    }
+
     private Position getStartPosition() {
         final String absolutePositionString = Prefs.getString(mContext,
                 Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
@@ -181,7 +228,7 @@
                 : Position.fromString(absolutePositionString);
     }
 
-    void registerContentObservers() {
+    void registerObserversAndCallbacks() {
         mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
                 /* notifyForDescendants */ false, mMenuTargetFeaturesContentObserver,
@@ -202,12 +249,20 @@
                 Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_OPACITY),
                 /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
                 UserHandle.USER_CURRENT);
+        mContext.registerComponentCallbacks(mComponentCallbacks);
+
+        mAccessibilityManager.addAccessibilityServicesStateChangeListener(
+                mA11yServicesStateChangeListener);
     }
 
-    void unregisterContentObservers() {
+    void unregisterObserversAndCallbacks() {
         mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver);
         mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver);
         mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver);
+        mContext.unregisterComponentCallbacks(mComponentCallbacks);
+
+        mAccessibilityManager.removeAccessibilityServicesStateChangeListener(
+                mA11yServicesStateChangeListener);
     }
 
     interface OnSettingsContentsChanged {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
index 9875ad0..3e2b06b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
@@ -20,6 +20,7 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import android.annotation.IntDef;
+import android.content.ComponentCallbacks;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
@@ -33,6 +34,8 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 
@@ -44,7 +47,7 @@
  * the {@link MenuView}.
  */
 class MenuMessageView extends LinearLayout implements
-        ViewTreeObserver.OnComputeInternalInsetsListener {
+        ViewTreeObserver.OnComputeInternalInsetsListener, ComponentCallbacks {
     private final TextView mTextView;
     private final Button mUndoButton;
 
@@ -61,6 +64,7 @@
     MenuMessageView(Context context) {
         super(context);
 
+        setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
         setVisibility(GONE);
 
         mTextView = new TextView(context);
@@ -72,13 +76,16 @@
     }
 
     @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
         updateResources();
     }
 
     @Override
+    public void onLowMemory() {
+        // Do nothing.
+    }
+
+    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
 
@@ -92,6 +99,7 @@
 
         updateResources();
 
+        getContext().registerComponentCallbacks(this);
         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
     }
 
@@ -99,6 +107,7 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
 
+        getContext().unregisterComponentCallbacks(this);
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index 986aa51..28269d9 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -19,6 +19,7 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import android.annotation.SuppressLint;
+import android.content.ComponentCallbacks;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.PointF;
@@ -46,7 +47,7 @@
  */
 @SuppressLint("ViewConstructor")
 class MenuView extends FrameLayout implements
-        ViewTreeObserver.OnComputeInternalInsetsListener {
+        ViewTreeObserver.OnComputeInternalInsetsListener, ComponentCallbacks {
     private static final int INDEX_MENU_ITEM = 0;
     private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>();
     private final AccessibilityTargetAdapter mAdapter;
@@ -106,14 +107,31 @@
     }
 
     @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
         loadLayoutResources();
 
         mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
     }
 
+    @Override
+    public void onLowMemory() {
+        // Do nothing.
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        getContext().registerComponentCallbacks(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        getContext().unregisterComponentCallbacks(this);
+    }
+
     void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) {
         mFeaturesChangeListener = listener;
     }
@@ -299,7 +317,7 @@
         mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver);
         mMenuViewModel.getMoveToTuckedData().observeForever(mMoveToTuckedObserver);
         setVisibility(VISIBLE);
-        mMenuViewModel.registerContentObservers();
+        mMenuViewModel.registerObserversAndCallbacks();
         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
         getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
     }
@@ -312,7 +330,7 @@
         mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver);
         mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver);
         mMenuViewModel.getMoveToTuckedData().removeObserver(mMoveToTuckedObserver);
-        mMenuViewModel.unregisterContentObservers();
+        mMenuViewModel.unregisterObserversAndCallbacks();
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
         getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 6f5b39c..15a8d09 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -25,20 +25,22 @@
 import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
 import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
 import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index;
+import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.IntDef;
 import android.annotation.StringDef;
 import android.annotation.SuppressLint;
+import android.content.ComponentCallbacks;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.util.PluralsMessageFormatter;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewTreeObserver;
@@ -61,9 +63,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 
 /**
@@ -74,7 +74,7 @@
  */
 @SuppressLint("ViewConstructor")
 class MenuViewLayer extends FrameLayout implements
-        ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener {
+        ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener, ComponentCallbacks {
     private static final int SHOW_MESSAGE_DELAY_MS = 3000;
 
     private final WindowManager mWindowManager;
@@ -137,8 +137,8 @@
                             AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
             serviceInfoList.forEach(info -> {
                 if (getAccessibilityServiceFragmentType(info) == INVISIBLE_TOGGLE) {
-                    setAccessibilityServiceState(mContext, info.getComponentName(), /* enabled= */
-                            false);
+                    setAccessibilityServiceState(getContext(),
+                            info.getComponentName(), /* enabled= */ false);
                 }
             });
 
@@ -150,11 +150,14 @@
             AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) {
         super(context);
 
+        // Simplifies the translation positioning and animations
+        setLayoutDirection(LAYOUT_DIRECTION_LTR);
+
         mWindowManager = windowManager;
         mAccessibilityManager = accessibilityManager;
         mFloatingMenu = floatingMenu;
 
-        mMenuViewModel = new MenuViewModel(context);
+        mMenuViewModel = new MenuViewModel(context, accessibilityManager);
         mMenuViewAppearance = new MenuViewAppearance(context, windowManager);
         mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance);
         mMenuAnimationController = mMenuView.getMenuAnimationController();
@@ -209,20 +212,30 @@
     }
 
     @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
         mDismissView.updateResources();
+        mDismissAnimationController.updateResources();
+    }
+
+    @Override
+    public void onLowMemory() {
+        // Do nothing.
     }
 
     private String getMessageText(List<AccessibilityTarget> newTargetFeatures) {
         Preconditions.checkArgument(newTargetFeatures.size() > 0,
                 "The list should at least have one feature.");
 
-        final Map<String, Object> arguments = new HashMap<>();
-        arguments.put("count", newTargetFeatures.size());
-        arguments.put("label", newTargetFeatures.get(0).getLabel());
-        return PluralsMessageFormatter.format(getResources(), arguments,
-                R.string.accessibility_floating_button_undo_message_text);
+        final int featuresSize = newTargetFeatures.size();
+        final Resources resources = getResources();
+        if (featuresSize == 1) {
+            return resources.getString(
+                    R.string.accessibility_floating_button_undo_message_label_text,
+                    newTargetFeatures.get(0).getLabel());
+        }
+
+        return icuMessageFormat(resources,
+                R.string.accessibility_floating_button_undo_message_number_text, featuresSize);
     }
 
     @Override
@@ -246,7 +259,7 @@
         mMenuViewModel.getMigrationTooltipVisibilityData().observeForever(
                 mMigrationTooltipObserver);
         mMessageView.setUndoListener(view -> undo());
-        mContext.registerComponentCallbacks(mDismissAnimationController);
+        getContext().registerComponentCallbacks(this);
     }
 
     @Override
@@ -261,7 +274,7 @@
         mMenuViewModel.getMigrationTooltipVisibilityData().removeObserver(
                 mMigrationTooltipObserver);
         mHandler.removeCallbacksAndMessages(/* token= */ null);
-        mContext.unregisterComponentCallbacks(mDismissAnimationController);
+        getContext().unregisterComponentCallbacks(this);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
index 5fea3b0..eec8467 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility.floatingmenu;
 
 import android.content.Context;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
@@ -41,8 +42,9 @@
     private final MutableLiveData<Position> mPercentagePositionData = new MutableLiveData<>();
     private final MenuInfoRepository mInfoRepository;
 
-    MenuViewModel(Context context) {
-        mInfoRepository = new MenuInfoRepository(context, /* settingsContentsChanged= */ this);
+    MenuViewModel(Context context, AccessibilityManager accessibilityManager) {
+        mInfoRepository = new MenuInfoRepository(context,
+                accessibilityManager, /* settingsContentsChanged= */ this);
     }
 
     @Override
@@ -111,11 +113,11 @@
         return mTargetFeaturesData;
     }
 
-    void registerContentObservers() {
-        mInfoRepository.registerContentObservers();
+    void registerObserversAndCallbacks() {
+        mInfoRepository.registerObserversAndCallbacks();
     }
 
-    void unregisterContentObservers() {
-        mInfoRepository.unregisterContentObservers();
+    void unregisterObserversAndCallbacks() {
+        mInfoRepository.unregisterObserversAndCallbacks();
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 76c1158..96bfa43 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -643,6 +643,7 @@
 
     @Provides
     @Singleton
+    @Nullable
     static BluetoothAdapter provideBluetoothAdapter(BluetoothManager bluetoothManager) {
         return bluetoothManager.getAdapter();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 44fd353..764a3d0 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -419,7 +419,13 @@
         unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true)
 
     // TODO(b/255697805): Tracking Bug
-    @JvmField val TRACKPAD_GESTURE_BACK = unreleasedFlag(1205, "trackpad_gesture_back", teamfood = false)
+    @JvmField
+    val TRACKPAD_GESTURE_BACK = unreleasedFlag(1205, "trackpad_gesture_back", teamfood = false)
+
+    // TODO(b/263826204): Tracking Bug
+    @JvmField
+    val WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM =
+        unreleasedFlag(1206, "persist.wm.debug.predictive_back_bouncer_anim", teamfood = true)
 
     // 1300 - screenshots
     // TODO(b/254512719): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
index 2a3a33e..2cf5fb9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -77,6 +77,7 @@
 
     /** Runnable to show the primary bouncer. */
     val showRunnable = Runnable {
+        repository.setPrimaryVisible(true)
         repository.setPrimaryShow(
             KeyguardBouncerModel(
                 promptReason = repository.bouncerPromptReason ?: 0,
@@ -85,7 +86,6 @@
             )
         )
         repository.setPrimaryShowingSoon(false)
-        repository.setPrimaryVisible(true)
         primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.VISIBLE)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
index 4d914fe..15fed32 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -49,9 +49,9 @@
         featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON_BACKEND) || useNewWifiIcon()
 
     /**
-     * Returns true if we should apply some coloring to the wifi icon that was rendered with the new
+     * Returns true if we should apply some coloring to the icons that were rendered with the new
      * pipeline to help with debugging.
      */
-    fun useWifiDebugColoring(): Boolean =
+    fun useDebugColoring(): Boolean =
         featureFlags.isEnabled(Flags.NEW_STATUS_BAR_ICONS_DEBUG_COLORING)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 0d01715..0993ab370 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
+import android.net.wifi.WifiManager
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.table.TableLogBuffer
@@ -35,8 +36,11 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.DisabledWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import dagger.Binds
@@ -78,9 +82,23 @@
     @ClassKey(MobileUiAdapter::class)
     abstract fun bindFeature(impl: MobileUiAdapter): CoreStartable
 
-    @Module
     companion object {
-        @JvmStatic
+        @Provides
+        @SysUISingleton
+        fun provideRealWifiRepository(
+            wifiManager: WifiManager?,
+            disabledWifiRepository: DisabledWifiRepository,
+            wifiRepositoryImplFactory: WifiRepositoryImpl.Factory,
+        ): RealWifiRepository {
+            // If we have a null [WifiManager], then the wifi repository should be permanently
+            // disabled.
+            return if (wifiManager == null) {
+                disabledWifiRepository
+            } else {
+                wifiRepositoryImplFactory.create(wifiManager)
+            }
+        }
+
         @Provides
         @SysUISingleton
         @WifiTableLog
@@ -88,7 +106,6 @@
             return factory.create("WifiTableLog", 100)
         }
 
-        @JvmStatic
         @Provides
         @SysUISingleton
         @AirplaneTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
index dd93541..5960387 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.model
 
 import android.telephony.Annotation.NetworkType
-import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 
 /**
@@ -26,21 +25,17 @@
  * methods on [MobileMappingsProxy] to generate an icon lookup key.
  */
 sealed interface ResolvedNetworkType {
-    @NetworkType val type: Int
     val lookupKey: String
 
     object UnknownNetworkType : ResolvedNetworkType {
-        override val type: Int = NETWORK_TYPE_UNKNOWN
         override val lookupKey: String = "unknown"
     }
 
     data class DefaultNetworkType(
-        @NetworkType override val type: Int,
         override val lookupKey: String,
     ) : ResolvedNetworkType
 
     data class OverrideNetworkType(
-        @NetworkType override val type: Int,
         override val lookupKey: String,
     ) : ResolvedNetworkType
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 40e9ba1..d04996b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
 import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyManager
 import com.android.systemui.log.table.TableLogBuffer
@@ -52,13 +51,12 @@
      * listener + model.
      */
     val connectionInfo: Flow<MobileConnectionModel>
+
+    /** The total number of levels. Used with [SignalDrawable]. */
+    val numberOfLevels: StateFlow<Int>
+
     /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
     val dataEnabled: StateFlow<Boolean>
-    /**
-     * True if this connection represents the default subscription per
-     * [SubscriptionManager.getDefaultDataSubscriptionId]
-     */
-    val isDefaultDataSubscription: StateFlow<Boolean>
 
     /**
      * See [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber]. This bit only matters if
@@ -70,4 +68,9 @@
 
     /** The service provider name for this network connection, or the default name */
     val networkName: StateFlow<NetworkNameModel>
+
+    companion object {
+        /** The default number of levels to use for [numberOfLevels]. */
+        const val DEFAULT_NUM_LEVELS = 4
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index 498c0b9..97b4c2c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -18,7 +18,6 @@
 
 import android.provider.Settings
 import android.telephony.CarrierConfigManager
-import android.telephony.SubscriptionManager
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.mobile.MobileMappings
 import com.android.settingslib.mobile.MobileMappings.Config
@@ -38,9 +37,6 @@
     /** Observable for the subscriptionId of the current mobile data connection */
     val activeMobileDataSubscriptionId: StateFlow<Int>
 
-    /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
-    val defaultDataSubId: StateFlow<Int>
-
     /** The current connectivity status for the default mobile network connection */
     val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
index db9d24f..0c8593d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
@@ -139,11 +139,6 @@
     override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
         activeRepo.flatMapLatest { it.defaultMobileIconGroup }
 
-    override val defaultDataSubId: StateFlow<Int> =
-        activeRepo
-            .flatMapLatest { it.defaultDataSubId }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
-
     override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
         activeRepo
             .flatMapLatest { it.defaultMobileNetworkConnectivity }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 0b5f9d5..0e164e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
@@ -139,14 +140,6 @@
 
     private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key }
 
-    // TODO(b/261029387): add a command for this value
-    override val defaultDataSubId =
-        activeMobileDataSubscriptionId.stateIn(
-            scope,
-            SharingStarted.WhileSubscribed(),
-            INVALID_SUBSCRIPTION_ID
-        )
-
     // TODO(b/261029387): not yet supported
     override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
 
@@ -199,7 +192,6 @@
         val connection = getRepoForSubId(subId)
         // This is always true here, because we split out disabled states at the data-source level
         connection.dataEnabled.value = true
-        connection.isDefaultDataSubscription.value = state.dataType != null
         connection.networkName.value = NetworkNameModel.Derived(state.name)
 
         connection.cdmaRoaming.value = state.roaming
@@ -261,15 +253,13 @@
 
     private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType {
         val key = mobileMappingsReverseLookup.value[this] ?: "dis"
-        return DefaultNetworkType(DEMO_NET_TYPE, key)
+        return DefaultNetworkType(key)
     }
 
     companion object {
         private const val TAG = "DemoMobileConnectionsRepo"
 
         private const val DEFAULT_SUB_ID = 1
-
-        private const val DEMO_NET_TYPE = 1234
     }
 }
 
@@ -279,9 +269,9 @@
 ) : MobileConnectionRepository {
     override val connectionInfo = MutableStateFlow(MobileConnectionModel())
 
-    override val dataEnabled = MutableStateFlow(true)
+    override val numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS)
 
-    override val isDefaultDataSubscription = MutableStateFlow(true)
+    override val dataEnabled = MutableStateFlow(true)
 
     override val cdmaRoaming = MutableStateFlow(false)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 5cfff82..0fa0fea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
@@ -63,6 +64,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onEach
@@ -78,7 +80,6 @@
     private val telephonyManager: TelephonyManager,
     private val globalSettings: GlobalSettings,
     broadcastDispatcher: BroadcastDispatcher,
-    defaultDataSubId: StateFlow<Int>,
     globalMobileDataSettingChangedEvent: Flow<Unit>,
     mobileMappingsProxy: MobileMappingsProxy,
     bgDispatcher: CoroutineDispatcher,
@@ -185,14 +186,12 @@
                                         OVERRIDE_NETWORK_TYPE_NONE
                                 ) {
                                     DefaultNetworkType(
-                                        telephonyDisplayInfo.networkType,
                                         mobileMappingsProxy.toIconKey(
                                             telephonyDisplayInfo.networkType
                                         )
                                     )
                                 } else {
                                     OverrideNetworkType(
-                                        telephonyDisplayInfo.overrideNetworkType,
                                         mobileMappingsProxy.toIconKeyOverride(
                                             telephonyDisplayInfo.overrideNetworkType
                                         )
@@ -214,6 +213,12 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), state)
     }
 
+    // This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
+    // once it's wired up inside of [CarrierConfigTracker].
+    override val numberOfLevels: StateFlow<Int> =
+        flowOf(DEFAULT_NUM_LEVELS)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
+
     /** Produces whenever the mobile data setting changes for this subId */
     private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
         val observer =
@@ -284,20 +289,6 @@
 
     private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
 
-    override val isDefaultDataSubscription: StateFlow<Boolean> = run {
-        val initialValue = defaultDataSubId.value == subId
-        defaultDataSubId
-            .mapLatest { it == subId }
-            .distinctUntilChanged()
-            .logDiffsForTable(
-                mobileLogger,
-                columnPrefix = "",
-                columnName = "isDefaultDataSub",
-                initialValue = initialValue,
-            )
-            .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue)
-    }
-
     class Factory
     @Inject
     constructor(
@@ -315,7 +306,6 @@
             subId: Int,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
-            defaultDataSubId: StateFlow<Int>,
             globalMobileDataSettingChangedEvent: Flow<Unit>,
         ): MobileConnectionRepository {
             val mobileLogger = logFactory.create(tableBufferLogName(subId), 100)
@@ -328,7 +318,6 @@
                 telephonyManager.createForSubscriptionId(subId),
                 globalSettings,
                 broadcastDispatcher,
-                defaultDataSubId,
                 globalMobileDataSettingChangedEvent,
                 mobileMappingsProxy,
                 bgDispatcher,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index d407abe..c88c700 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -35,7 +35,6 @@
 import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
 import android.telephony.TelephonyManager
 import androidx.annotation.VisibleForTesting
-import com.android.internal.telephony.PhoneConstants
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.mobile.MobileMappings.Config
 import com.android.systemui.R
@@ -60,7 +59,6 @@
 import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -142,24 +140,10 @@
             .logInputChange(logger, "onActiveDataSubscriptionIdChanged")
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
 
-    private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
-        MutableSharedFlow(extraBufferCapacity = 1)
-
-    override val defaultDataSubId: StateFlow<Int> =
+    private val defaultDataSubIdChangedEvent =
         broadcastDispatcher
-            .broadcastFlow(
-                IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
-            ) { intent, _ ->
-                intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
-            }
-            .distinctUntilChanged()
+            .broadcastFlow(IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED))
             .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED")
-            .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
-            .stateIn(
-                scope,
-                SharingStarted.WhileSubscribed(),
-                SubscriptionManager.getDefaultDataSubscriptionId()
-            )
 
     private val carrierConfigChangedEvent =
         broadcastDispatcher
@@ -167,7 +151,7 @@
             .logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED")
 
     override val defaultDataSubRatConfig: StateFlow<Config> =
-        merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
+        merge(defaultDataSubIdChangedEvent, carrierConfigChangedEvent)
             .mapLatest { Config.readConfig(context) }
             .distinctUntilChanged()
             .logInputChange(logger, "defaultDataSubRatConfig")
@@ -272,7 +256,6 @@
             subId,
             defaultNetworkName,
             networkNameSeparator,
-            defaultDataSubId,
             globalMobileDataSettingChangedEvent,
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 31ac7a1..9427c6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -23,12 +23,11 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
-import com.android.systemui.util.CarrierConfigTracker
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
@@ -171,11 +170,12 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
 
-    /**
-     * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
-     * once it's wired up inside of [CarrierConfigTracker]
-     */
-    override val numberOfLevels: StateFlow<Int> = MutableStateFlow(4)
+    override val numberOfLevels: StateFlow<Int> =
+        connectionRepository.numberOfLevels.stateIn(
+            scope,
+            SharingStarted.WhileSubscribed(),
+            connectionRepository.numberOfLevels.value,
+        )
 
     override val isDataConnected: StateFlow<Boolean> =
         connectionInfo
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index ab442b5..3e81c7c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -19,6 +19,7 @@
 import android.content.res.ColorStateList
 import android.view.View
 import android.view.View.GONE
+import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.widget.ImageView
@@ -30,7 +31,13 @@
 import com.android.systemui.R
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
+import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.launch
 
@@ -40,7 +47,8 @@
     fun bind(
         view: ViewGroup,
         viewModel: LocationBasedMobileViewModel,
-    ) {
+    ): ModernStatusBarViewBinding {
+        val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group)
         val activityContainer = view.requireViewById<View>(R.id.inout_container)
         val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)
         val activityOut = view.requireViewById<ImageView>(R.id.mobile_out)
@@ -49,12 +57,39 @@
         val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
         val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
         val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
+        val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
 
         view.isVisible = true
         iconView.isVisible = true
 
+        // TODO(b/238425913): We should log this visibility state.
+        @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 {
+                    visibilityState.collect { state ->
+                        when (state) {
+                            STATE_ICON -> {
+                                mobileGroupView.visibility = VISIBLE
+                                dotView.visibility = GONE
+                            }
+                            STATE_DOT -> {
+                                mobileGroupView.visibility = INVISIBLE
+                                dotView.visibility = VISIBLE
+                            }
+                            STATE_HIDDEN -> {
+                                mobileGroupView.visibility = INVISIBLE
+                                dotView.visibility = INVISIBLE
+                            }
+                        }
+                    }
+                }
+
                 // Set the icon for the triangle
                 launch {
                     viewModel.iconId.distinctUntilChanged().collect { iconId ->
@@ -89,15 +124,43 @@
 
                 // Set the tint
                 launch {
-                    viewModel.tint.collect { tint ->
+                    iconTint.collect { tint ->
                         val tintList = ColorStateList.valueOf(tint)
                         iconView.imageTintList = tintList
                         networkTypeView.imageTintList = tintList
                         roamingView.imageTintList = tintList
                         activityIn.imageTintList = tintList
                         activityOut.imageTintList = tintList
+                        dotView.setDecorColor(tint)
                     }
                 }
+
+                launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } }
+            }
+        }
+
+        return object : ModernStatusBarViewBinding {
+            override fun getShouldIconBeVisible(): Boolean {
+                // If this view model exists, then the icon should be visible.
+                return true
+            }
+
+            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/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
index e86fee2..ed9a188 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -17,50 +17,20 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.view
 
 import android.content.Context
-import android.graphics.Rect
 import android.util.AttributeSet
 import android.view.LayoutInflater
 import com.android.systemui.R
-import com.android.systemui.statusbar.BaseStatusBarFrameLayout
-import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
-import java.util.ArrayList
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
 
 class ModernStatusBarMobileView(
     context: Context,
     attrs: AttributeSet?,
-) : BaseStatusBarFrameLayout(context, attrs) {
+) : ModernStatusBarView(context, attrs) {
 
     var subId: Int = -1
 
-    private lateinit var slot: String
-    override fun getSlot() = slot
-
-    override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
-        // TODO
-    }
-
-    override fun setStaticDrawableColor(color: Int) {
-        // TODO
-    }
-
-    override fun setDecorColor(color: Int) {
-        // TODO
-    }
-
-    override fun setVisibleState(state: Int, animate: Boolean) {
-        // TODO
-    }
-
-    override fun getVisibleState(): Int {
-        return STATE_ICON
-    }
-
-    override fun isIconVisible(): Boolean {
-        return true
-    }
-
     companion object {
 
         /**
@@ -77,9 +47,8 @@
                     .inflate(R.layout.status_bar_mobile_signal_group_new, null)
                     as ModernStatusBarMobileView)
                 .also {
-                    it.slot = slot
                     it.subId = viewModel.subscriptionId
-                    MobileIconBinder.bind(it, viewModel)
+                    it.initView(slot) { MobileIconBinder.bind(it, viewModel) }
                 }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
index b0dc41f..24cd930 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
@@ -18,11 +18,7 @@
 
 import android.graphics.Color
 import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 
 /**
  * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This
@@ -33,50 +29,51 @@
  */
 abstract class LocationBasedMobileViewModel(
     val commonImpl: MobileIconViewModelCommon,
-    val logger: ConnectivityPipelineLogger,
+    statusBarPipelineFlags: StatusBarPipelineFlags,
+    debugTint: Int,
 ) : MobileIconViewModelCommon by commonImpl {
-    abstract val tint: Flow<Int>
+    val useDebugColoring: Boolean = statusBarPipelineFlags.useDebugColoring()
+
+    val defaultColor: Int =
+        if (useDebugColoring) {
+            debugTint
+        } else {
+            Color.WHITE
+        }
 
     companion object {
         fun viewModelForLocation(
             commonImpl: MobileIconViewModelCommon,
-            logger: ConnectivityPipelineLogger,
+            statusBarPipelineFlags: StatusBarPipelineFlags,
             loc: StatusBarLocation,
         ): LocationBasedMobileViewModel =
             when (loc) {
-                StatusBarLocation.HOME -> HomeMobileIconViewModel(commonImpl, logger)
-                StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl, logger)
-                StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, logger)
+                StatusBarLocation.HOME ->
+                    HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags)
+                StatusBarLocation.KEYGUARD ->
+                    KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags)
+                StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, statusBarPipelineFlags)
             }
     }
 }
 
 class HomeMobileIconViewModel(
     commonImpl: MobileIconViewModelCommon,
-    logger: ConnectivityPipelineLogger,
-) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
-    override val tint: Flow<Int> =
-        flowOf(Color.CYAN)
-            .distinctUntilChanged()
-            .logOutputChange(logger, "HOME tint(${commonImpl.subscriptionId})")
-}
+    statusBarPipelineFlags: StatusBarPipelineFlags,
+) :
+    MobileIconViewModelCommon,
+    LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.CYAN)
 
 class QsMobileIconViewModel(
     commonImpl: MobileIconViewModelCommon,
-    logger: ConnectivityPipelineLogger,
-) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
-    override val tint: Flow<Int> =
-        flowOf(Color.GREEN)
-            .distinctUntilChanged()
-            .logOutputChange(logger, "QS tint(${commonImpl.subscriptionId})")
-}
+    statusBarPipelineFlags: StatusBarPipelineFlags,
+) :
+    MobileIconViewModelCommon,
+    LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.GREEN)
 
 class KeyguardMobileIconViewModel(
     commonImpl: MobileIconViewModelCommon,
-    logger: ConnectivityPipelineLogger,
-) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
-    override val tint: Flow<Int> =
-        flowOf(Color.MAGENTA)
-            .distinctUntilChanged()
-            .logOutputChange(logger, "KEYGUARD tint(${commonImpl.subscriptionId})")
-}
+    statusBarPipelineFlags: StatusBarPipelineFlags,
+) :
+    MobileIconViewModelCommon,
+    LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.MAGENTA)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index b9318b1..24370d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
@@ -41,6 +42,7 @@
     private val logger: ConnectivityPipelineLogger,
     private val constants: ConnectivityConstants,
     @Application private val scope: CoroutineScope,
+    private val statusBarPipelineFlags: StatusBarPipelineFlags,
 ) {
     @VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>()
 
@@ -60,7 +62,11 @@
                     )
                     .also { mobileIconSubIdCache[subId] = it }
 
-        return LocationBasedMobileViewModel.viewModelForLocation(common, logger, location)
+        return LocationBasedMobileViewModel.viewModelForLocation(
+            common,
+            statusBarPipelineFlags,
+            location,
+        )
     }
 
     private fun removeInvalidModelsFromCache(subIds: List<Int>) {
@@ -75,6 +81,7 @@
         private val logger: ConnectivityPipelineLogger,
         private val constants: ConnectivityConstants,
         @Application private val scope: CoroutineScope,
+        private val statusBarPipelineFlags: StatusBarPipelineFlags,
     ) {
         fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel {
             return MobileIconsViewModel(
@@ -83,6 +90,7 @@
                 logger,
                 constants,
                 scope,
+                statusBarPipelineFlags,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt
new file mode 100644
index 0000000..f67876b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.binder
+
+import com.android.systemui.statusbar.StatusBarIconView
+
+/**
+ * Defines interface for an object that acts as the binding between a modern status bar view and its
+ * view-model.
+ *
+ * Users of the view binder classes in the modern status bar pipeline should use this to control the
+ * binder after it is bound.
+ */
+interface ModernStatusBarViewBinding {
+    /** Returns true if the icon should be visible and false otherwise. */
+    fun getShouldIconBeVisible(): Boolean
+
+    /** 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)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
new file mode 100644
index 0000000..cc0ec54
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
@@ -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 com.android.systemui.statusbar.pipeline.shared.ui.view
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.Gravity
+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
+import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+
+/**
+ * A new and more modern implementation of [BaseStatusBarFrameLayout] that gets updated by view
+ * binders communicating via [ModernStatusBarViewBinding].
+ */
+open class ModernStatusBarView(context: Context, attrs: AttributeSet?) :
+    BaseStatusBarFrameLayout(context, attrs) {
+
+    private lateinit var slot: String
+    private lateinit var binding: ModernStatusBarViewBinding
+
+    @StatusBarIconView.VisibleState
+    private var iconVisibleState: Int = STATE_HIDDEN
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            binding.onVisibilityStateChanged(value)
+        }
+
+    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) {
+        binding.onIconTintChanged(color)
+    }
+
+    override fun setDecorColor(color: Int) {
+        binding.onDecorTintChanged(color)
+    }
+
+    override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) {
+        iconVisibleState = state
+    }
+
+    @StatusBarIconView.VisibleState
+    override fun getVisibleState(): Int {
+        return iconVisibleState
+    }
+
+    override fun isIconVisible(): Boolean {
+        return binding.getShouldIconBeVisible()
+    }
+
+    /**
+     * Initializes this view.
+     *
+     * Creates a dot view, and uses [bindingCreator] to get and set the binding.
+     */
+    fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
+        // The dot view requires [slot] to be set, and the [binding] may require an instantiated dot
+        // view. So, this is the required order.
+        this.slot = slot
+        initDotView()
+        this.binding = bindingCreator.invoke()
+    }
+
+    /**
+     * Creates a [StatusBarIconView] that is always in DOT mode and adds it to this view.
+     *
+     * Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView] and
+     * [com.android.systemui.statusbar.StatusBarMobileView].
+     */
+    private fun initDotView() {
+        // TODO(b/238425913): Could we just have this dot view be part of the layout with a dot
+        //  drawable so we don't need to inflate it manually? Would that not work with animations?
+        val dotView =
+            StatusBarIconView(mContext, slot, null).also {
+                it.id = R.id.status_bar_dot
+                // Hard-code this view to always be in the DOT state so that whenever it's visible
+                // it will show a dot
+                it.visibleState = STATE_DOT
+            }
+
+        val width = mContext.resources.getDimensionPixelSize(R.dimen.status_bar_icon_size)
+        val lp = LayoutParams(width, width)
+        lp.gravity = Gravity.CENTER_VERTICAL or Gravity.START
+        addView(dotView, lp)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index a682a57..4251d18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -23,6 +23,33 @@
 /** Provides information about the current wifi network. */
 sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
 
+    /**
+     * A model representing that we couldn't fetch any wifi information.
+     *
+     * This is only used with [DisabledWifiRepository], where [WifiManager] is null.
+     */
+    object Unavailable : WifiNetworkModel() {
+        override fun toString() = "WifiNetwork.Unavailable"
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal is Unavailable) {
+                return
+            }
+
+            logFull(row)
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE)
+            row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+            row.logChange(COL_VALIDATED, false)
+            row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+            row.logChange(COL_SSID, null)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+            row.logChange(COL_PASSPOINT_NAME, null)
+        }
+    }
+
     /** A model representing that we have no active wifi network. */
     object Inactive : WifiNetworkModel() {
         override fun toString() = "WifiNetwork.Inactive"
@@ -87,13 +114,8 @@
 
         /**
          * The wifi signal level, guaranteed to be 0 <= level <= 4.
-         *
-         * Null if we couldn't fetch the level for some reason.
-         *
-         * TODO(b/238425913): The level will only be null if we have a null WifiManager. Is there a
-         *   way we can guarantee a non-null WifiManager?
          */
-        val level: Int? = null,
+        val level: Int,
 
         /** See [android.net.wifi.WifiInfo.ssid]. */
         val ssid: String? = null,
@@ -108,7 +130,7 @@
         val passpointProviderFriendlyName: String? = null,
     ) : WifiNetworkModel() {
         init {
-            require(level == null || level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) {
+            require(level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) {
                 "0 <= wifi level <= 4 required; level was $level"
             }
         }
@@ -125,11 +147,7 @@
                 row.logChange(COL_VALIDATED, isValidated)
             }
             if (prevVal !is Active || prevVal.level != level) {
-                if (level != null) {
-                    row.logChange(COL_LEVEL, level)
-                } else {
-                    row.logChange(COL_LEVEL, LEVEL_DEFAULT)
-                }
+                row.logChange(COL_LEVEL, level)
             }
             if (prevVal !is Active || prevVal.ssid != ssid) {
                 row.logChange(COL_SSID, ssid)
@@ -190,6 +208,7 @@
 }
 
 const val TYPE_CARRIER_MERGED = "CarrierMerged"
+const val TYPE_UNAVAILABLE = "Unavailable"
 const val TYPE_INACTIVE = "Inactive"
 const val TYPE_ACTIVE = "Active"
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 53525f2..ac4d55c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -34,3 +34,13 @@
     /** Observable for the current wifi network activity. */
     val wifiActivity: StateFlow<DataActivityModel>
 }
+
+/**
+ * A no-op interface used for Dagger bindings.
+ *
+ * [WifiRepositorySwitcher] needs to inject the "real" wifi repository, which could either be the
+ * full [WifiRepositoryImpl] or just [DisabledWifiRepository]. Having this interface lets us bind
+ * [RealWifiRepository], and then [WifiRepositorySwitcher] will automatically get the correct real
+ * repository.
+ */
+interface RealWifiRepository : WifiRepository
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
index be86620..2cb81c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
@@ -58,7 +58,7 @@
 class WifiRepositorySwitcher
 @Inject
 constructor(
-    private val realImpl: WifiRepositoryImpl,
+    private val realImpl: RealWifiRepository,
     private val demoImpl: DemoWifiRepository,
     private val demoModeController: DemoModeController,
     @Application scope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
index 7890074..be3d7d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
@@ -89,7 +89,7 @@
         WifiNetworkModel.Active(
             networkId = DEMO_NET_ID,
             isValidated = validated ?: true,
-            level = level,
+            level = level ?: 0,
             ssid = ssid,
 
             // These fields below aren't supported in demo mode, since they aren't needed to satisfy
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt
new file mode 100644
index 0000000..5d4a666
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * Implementation of wifi repository used when wifi is permanently disabled on the device.
+ *
+ * This repo should only exist when [WifiManager] is null, which means that we can never fetch any
+ * wifi information.
+ */
+@SysUISingleton
+class DisabledWifiRepository @Inject constructor() : RealWifiRepository {
+    override val isWifiEnabled: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
+
+    override val isWifiDefault: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
+
+    override val wifiNetwork: StateFlow<WifiNetworkModel> = MutableStateFlow(NETWORK).asStateFlow()
+
+    override val wifiActivity: StateFlow<DataActivityModel> =
+        MutableStateFlow(ACTIVITY).asStateFlow()
+
+    companion object {
+        private val NETWORK = WifiNetworkModel.Unavailable
+        private val ACTIVITY = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index c8c94e1..c47c20d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -29,7 +29,6 @@
 import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.TrafficStateCallback
-import android.util.Log
 import com.android.settingslib.Utils
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -40,11 +39,11 @@
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -53,12 +52,9 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.stateIn
@@ -68,178 +64,177 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 @SuppressLint("MissingPermission")
-class WifiRepositoryImpl @Inject constructor(
+class WifiRepositoryImpl
+@Inject
+constructor(
     broadcastDispatcher: BroadcastDispatcher,
     connectivityManager: ConnectivityManager,
     logger: ConnectivityPipelineLogger,
     @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
     @Main mainExecutor: Executor,
     @Application scope: CoroutineScope,
-    wifiManager: WifiManager?,
-) : WifiRepository {
+    wifiManager: WifiManager,
+) : RealWifiRepository {
 
-    private val wifiStateChangeEvents: Flow<Unit> = broadcastDispatcher.broadcastFlow(
-        IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)
-    )
-        .logInputChange(logger, "WIFI_STATE_CHANGED_ACTION intent")
+    private val wifiStateChangeEvents: Flow<Unit> =
+        broadcastDispatcher
+            .broadcastFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION))
+            .logInputChange(logger, "WIFI_STATE_CHANGED_ACTION intent")
 
     private val wifiNetworkChangeEvents: MutableSharedFlow<Unit> =
         MutableSharedFlow(extraBufferCapacity = 1)
 
+    // Because [WifiManager] doesn't expose a wifi enabled change listener, we do it
+    // internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may
+    // have changed.
     override val isWifiEnabled: StateFlow<Boolean> =
-        if (wifiManager == null) {
-            MutableStateFlow(false).asStateFlow()
-        } else {
-            // Because [WifiManager] doesn't expose a wifi enabled change listener, we do it
-            // internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may
-            // have changed.
-            merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
-                .mapLatest { wifiManager.isWifiEnabled }
-                .distinctUntilChanged()
-                .logDiffsForTable(
-                    wifiTableLogBuffer,
-                    columnPrefix = "",
-                    columnName = "isWifiEnabled",
-                    initialValue = wifiManager.isWifiEnabled,
-                )
-                .stateIn(
-                    scope = scope,
-                    started = SharingStarted.WhileSubscribed(),
-                    initialValue = wifiManager.isWifiEnabled
-                )
-        }
+        merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
+            .mapLatest { wifiManager.isWifiEnabled }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                wifiTableLogBuffer,
+                columnPrefix = "",
+                columnName = "isWifiEnabled",
+                initialValue = wifiManager.isWifiEnabled,
+            )
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = wifiManager.isWifiEnabled,
+            )
 
-    override val isWifiDefault: StateFlow<Boolean> = conflatedCallbackFlow {
-        // Note: This callback doesn't do any logging because we already log every network change
-        // in the [wifiNetwork] callback.
-        val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
-            override fun onCapabilitiesChanged(
-                network: Network,
-                networkCapabilities: NetworkCapabilities
-            ) {
-                // This method will always be called immediately after the network becomes the
-                // default, in addition to any time the capabilities change while the network is
-                // the default.
-                // If this network contains valid wifi info, then wifi is the default network.
-                val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
-                trySend(wifiInfo != null)
+    override val isWifiDefault: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                // Note: This callback doesn't do any logging because we already log every network
+                // change in the [wifiNetwork] callback.
+                val callback =
+                    object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+                        override fun onCapabilitiesChanged(
+                            network: Network,
+                            networkCapabilities: NetworkCapabilities
+                        ) {
+                            // This method will always be called immediately after the network
+                            // becomes the default, in addition to any time the capabilities change
+                            // while the network is the default.
+                            // If this network contains valid wifi info, then wifi is the default
+                            // network.
+                            val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
+                            trySend(wifiInfo != null)
+                        }
+
+                        override fun onLost(network: Network) {
+                            // The system no longer has a default network, so wifi is definitely not
+                            // default.
+                            trySend(false)
+                        }
+                    }
+
+                connectivityManager.registerDefaultNetworkCallback(callback)
+                awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
             }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                wifiTableLogBuffer,
+                columnPrefix = "",
+                columnName = "isWifiDefault",
+                initialValue = false,
+            )
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
 
-            override fun onLost(network: Network) {
-                // The system no longer has a default network, so wifi is definitely not default.
-                trySend(false)
+    override val wifiNetwork: StateFlow<WifiNetworkModel> =
+        conflatedCallbackFlow {
+                var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
+
+                val callback =
+                    object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+                        override fun onCapabilitiesChanged(
+                            network: Network,
+                            networkCapabilities: NetworkCapabilities
+                        ) {
+                            logger.logOnCapabilitiesChanged(network, networkCapabilities)
+
+                            wifiNetworkChangeEvents.tryEmit(Unit)
+
+                            val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
+                            if (wifiInfo?.isPrimary == true) {
+                                val wifiNetworkModel =
+                                    createWifiNetworkModel(
+                                        wifiInfo,
+                                        network,
+                                        networkCapabilities,
+                                        wifiManager,
+                                    )
+                                logger.logTransformation(
+                                    WIFI_NETWORK_CALLBACK_NAME,
+                                    oldValue = currentWifi,
+                                    newValue = wifiNetworkModel,
+                                )
+                                currentWifi = wifiNetworkModel
+                                trySend(wifiNetworkModel)
+                            }
+                        }
+
+                        override fun onLost(network: Network) {
+                            logger.logOnLost(network)
+
+                            wifiNetworkChangeEvents.tryEmit(Unit)
+
+                            val wifi = currentWifi
+                            if (
+                                wifi is WifiNetworkModel.Active &&
+                                    wifi.networkId == network.getNetId()
+                            ) {
+                                val newNetworkModel = WifiNetworkModel.Inactive
+                                logger.logTransformation(
+                                    WIFI_NETWORK_CALLBACK_NAME,
+                                    oldValue = wifi,
+                                    newValue = newNetworkModel,
+                                )
+                                currentWifi = newNetworkModel
+                                trySend(newNetworkModel)
+                            }
+                        }
+                    }
+
+                connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback)
+
+                awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
             }
-        }
-
-        connectivityManager.registerDefaultNetworkCallback(callback)
-        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
-    }
-        .distinctUntilChanged()
-        .logDiffsForTable(
-            wifiTableLogBuffer,
-            columnPrefix = "",
-            columnName = "isWifiDefault",
-            initialValue = false,
-        )
-        .stateIn(
-            scope,
-            started = SharingStarted.WhileSubscribed(),
-            initialValue = false
-        )
-
-    override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow {
-        var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
-
-        val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
-            override fun onCapabilitiesChanged(
-                network: Network,
-                networkCapabilities: NetworkCapabilities
-            ) {
-                logger.logOnCapabilitiesChanged(network, networkCapabilities)
-
-                wifiNetworkChangeEvents.tryEmit(Unit)
-
-                val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
-                if (wifiInfo?.isPrimary == true) {
-                    val wifiNetworkModel = createWifiNetworkModel(
-                        wifiInfo,
-                        network,
-                        networkCapabilities,
-                        wifiManager,
-                    )
-                    logger.logTransformation(
-                        WIFI_NETWORK_CALLBACK_NAME,
-                        oldValue = currentWifi,
-                        newValue = wifiNetworkModel
-                    )
-                    currentWifi = wifiNetworkModel
-                    trySend(wifiNetworkModel)
-                }
-            }
-
-            override fun onLost(network: Network) {
-                logger.logOnLost(network)
-
-                wifiNetworkChangeEvents.tryEmit(Unit)
-
-                val wifi = currentWifi
-                if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) {
-                    val newNetworkModel = WifiNetworkModel.Inactive
-                    logger.logTransformation(
-                        WIFI_NETWORK_CALLBACK_NAME,
-                        oldValue = wifi,
-                        newValue = newNetworkModel
-                    )
-                    currentWifi = newNetworkModel
-                    trySend(newNetworkModel)
-                }
-            }
-        }
-
-        connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback)
-
-        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
-    }
-        .distinctUntilChanged()
-        .logDiffsForTable(
-            wifiTableLogBuffer,
-            columnPrefix = "wifiNetwork",
-            initialValue = WIFI_NETWORK_DEFAULT,
-        )
-        // There will be multiple wifi icons in different places that will frequently
-        // subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures that
-        // new subscribes will get the latest value immediately upon subscription. Otherwise, the
-        // views could show stale data. See b/244173280.
-        .stateIn(
-            scope,
-            started = SharingStarted.WhileSubscribed(),
-            initialValue = WIFI_NETWORK_DEFAULT
-        )
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                wifiTableLogBuffer,
+                columnPrefix = "wifiNetwork",
+                initialValue = WIFI_NETWORK_DEFAULT,
+            )
+            // There will be multiple wifi icons in different places that will frequently
+            // subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures
+            // that new subscribes will get the latest value immediately upon subscription.
+            // Otherwise, the views could show stale data. See b/244173280.
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = WIFI_NETWORK_DEFAULT,
+            )
 
     override val wifiActivity: StateFlow<DataActivityModel> =
-            if (wifiManager == null) {
-                Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback")
-                flowOf(ACTIVITY_DEFAULT)
-            } else {
-                conflatedCallbackFlow {
-                    val callback = TrafficStateCallback { state ->
-                        logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
-                        trySend(state.toWifiDataActivityModel())
-                    }
-                    wifiManager.registerTrafficStateCallback(mainExecutor, callback)
-                    awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
+        conflatedCallbackFlow {
+                val callback = TrafficStateCallback { state ->
+                    logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
+                    trySend(state.toWifiDataActivityModel())
                 }
+                wifiManager.registerTrafficStateCallback(mainExecutor, callback)
+                awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
             }
-                .logDiffsForTable(
-                    wifiTableLogBuffer,
-                    columnPrefix = ACTIVITY_PREFIX,
-                    initialValue = ACTIVITY_DEFAULT,
-                )
-                .stateIn(
-                    scope,
-                    started = SharingStarted.WhileSubscribed(),
-                    initialValue = ACTIVITY_DEFAULT
-                )
+            .logDiffsForTable(
+                wifiTableLogBuffer,
+                columnPrefix = ACTIVITY_PREFIX,
+                initialValue = ACTIVITY_DEFAULT,
+            )
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = ACTIVITY_DEFAULT,
+            )
 
     companion object {
         private const val ACTIVITY_PREFIX = "wifiActivity"
@@ -271,19 +266,19 @@
             wifiInfo: WifiInfo,
             network: Network,
             networkCapabilities: NetworkCapabilities,
-            wifiManager: WifiManager?,
+            wifiManager: WifiManager,
         ): WifiNetworkModel {
             return if (wifiInfo.isCarrierMerged) {
                 WifiNetworkModel.CarrierMerged
             } else {
                 WifiNetworkModel.Active(
-                        network.getNetId(),
-                        isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED),
-                        level = wifiManager?.calculateSignalLevel(wifiInfo.rssi),
-                        wifiInfo.ssid,
-                        wifiInfo.isPasspointAp,
-                        wifiInfo.isOsuAp,
-                        wifiInfo.passpointProviderFriendlyName
+                    network.getNetId(),
+                    isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED),
+                    level = wifiManager.calculateSignalLevel(wifiInfo.rssi),
+                    wifiInfo.ssid,
+                    wifiInfo.isPasspointAp,
+                    wifiInfo.isOsuAp,
+                    wifiInfo.passpointProviderFriendlyName
                 )
             }
         }
@@ -308,4 +303,28 @@
 
         private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel"
     }
+
+    @SysUISingleton
+    class Factory
+    @Inject
+    constructor(
+        private val broadcastDispatcher: BroadcastDispatcher,
+        private val connectivityManager: ConnectivityManager,
+        private val logger: ConnectivityPipelineLogger,
+        @WifiTableLog private val wifiTableLogBuffer: TableLogBuffer,
+        @Main private val mainExecutor: Executor,
+        @Application private val scope: CoroutineScope,
+    ) {
+        fun create(wifiManager: WifiManager): WifiRepositoryImpl {
+            return WifiRepositoryImpl(
+                broadcastDispatcher,
+                connectivityManager,
+                logger,
+                wifiTableLogBuffer,
+                mainExecutor,
+                scope,
+                wifiManager,
+            )
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 93041ce..980560a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -65,6 +65,7 @@
 
     override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info ->
         when (info) {
+            is WifiNetworkModel.Unavailable -> null
             is WifiNetworkModel.Inactive -> null
             is WifiNetworkModel.CarrierMerged -> null
             is WifiNetworkModel.Active -> when {
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 cc67c84..2aff12c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 import kotlinx.coroutines.InternalCoroutinesApi
@@ -49,31 +50,12 @@
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 object WifiViewBinder {
 
-    /**
-     * Defines interface for an object that acts as the binding between the view and its view-model.
-     *
-     * Users of the [WifiViewBinder] class should use this to control the binder after it is bound.
-     */
-    interface Binding {
-        /** Returns true if the wifi icon should be visible and false otherwise. */
-        fun getShouldIconBeVisible(): Boolean
-
-        /** 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. */
     @JvmStatic
     fun bind(
         view: ViewGroup,
         viewModel: LocationBasedWifiViewModel,
-    ): Binding {
+    ): ModernStatusBarViewBinding {
         val groupView = view.requireViewById<ViewGroup>(R.id.wifi_group)
         val iconView = view.requireViewById<ImageView>(R.id.wifi_signal)
         val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
@@ -148,7 +130,7 @@
             }
         }
 
-        return object : Binding {
+        return object : ModernStatusBarViewBinding {
             override fun getShouldIconBeVisible(): Boolean {
                 return viewModel.wifiIcon.value is WifiIcon.Visible
             }
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 be7782c..7a73486 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
@@ -16,17 +16,12 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.ui.view
 
+import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.Rect
 import android.util.AttributeSet
-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
-import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
 import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 
@@ -36,83 +31,14 @@
  */
 class ModernStatusBarWifiView(
     context: Context,
-    attrs: AttributeSet?
-) : BaseStatusBarFrameLayout(context, attrs) {
-
-    private lateinit var slot: String
-    private lateinit var binding: WifiViewBinder.Binding
-
-    @StatusBarIconView.VisibleState
-    private var iconVisibleState: Int = STATE_HIDDEN
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            binding.onVisibilityStateChanged(value)
-        }
-
-    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) {
-        binding.onIconTintChanged(color)
-    }
-
-    override fun setDecorColor(color: Int) {
-        binding.onDecorTintChanged(color)
-    }
-
-    override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) {
-        iconVisibleState = state
-    }
-
-    @StatusBarIconView.VisibleState
-    override fun getVisibleState(): Int {
-        return iconVisibleState
-    }
-
-    override fun isIconVisible(): Boolean {
-        return binding.getShouldIconBeVisible()
-    }
-
-    private fun initView(
-        slotName: String,
-        wifiViewModel: LocationBasedWifiViewModel,
-    ) {
-        slot = slotName
-        initDotView()
-        binding = WifiViewBinder.bind(this, wifiViewModel)
-    }
-
-    // Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView].
-    private fun initDotView() {
-        // TODO(b/238425913): Could we just have this dot view be part of
-        //   R.layout.new_status_bar_wifi_group with a dot drawable so we don't need to inflate it
-        //   manually? Would that not work with animations?
-        val dotView = StatusBarIconView(mContext, slot, null).also {
-            it.id = R.id.status_bar_dot
-            // Hard-code this view to always be in the DOT state so that whenever it's visible it
-            // will show a dot
-            it.visibleState = STATE_DOT
-        }
-
-        val width = mContext.resources.getDimensionPixelSize(R.dimen.status_bar_icon_size)
-        val lp = LayoutParams(width, width)
-        lp.gravity = Gravity.CENTER_VERTICAL or Gravity.START
-        addView(dotView, lp)
-    }
-
+    attrs: AttributeSet?,
+) : ModernStatusBarView(context, attrs) {
     companion object {
         /**
          * Inflates a new instance of [ModernStatusBarWifiView], binds it to a view model, and
          * returns it.
          */
+        @SuppressLint("InflateParams")
         @JvmStatic
         fun constructAndBind(
             context: Context,
@@ -123,7 +49,7 @@
                 LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
                     as ModernStatusBarWifiView
                 ).also {
-                    it.initView(slot, wifiViewModel)
+                    it.initView(slot) { WifiViewBinder.bind(it, wifiViewModel) }
                 }
         }
     }
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 a4615cc..02c3a65 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
@@ -47,7 +47,7 @@
     /** True if the airplane spacer view should be visible. */
     val isAirplaneSpacerVisible: Flow<Boolean>,
 ) {
-    val useDebugColoring: Boolean = statusBarPipelineFlags.useWifiDebugColoring()
+    val useDebugColoring: Boolean = statusBarPipelineFlags.useDebugColoring()
 
     val defaultColor: Int =
         if (useDebugColoring) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index ab464cc..824b597 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -82,6 +82,7 @@
     /** Returns the icon to use based on the given network. */
     private fun WifiNetworkModel.icon(): WifiIcon {
         return when (this) {
+            is WifiNetworkModel.Unavailable -> WifiIcon.Hidden
             is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden
             is WifiNetworkModel.Inactive -> WifiIcon.Visible(
                 res = WIFI_NO_NETWORK,
@@ -89,27 +90,23 @@
                     "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}"
                 )
             )
-            is WifiNetworkModel.Active ->
-                when (this.level) {
-                    null -> WifiIcon.Hidden
-                    else -> {
-                        val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level])
-                        when {
-                            this.isValidated ->
-                                WifiIcon.Visible(
-                                    WIFI_FULL_ICONS[this.level],
-                                    ContentDescription.Loaded(levelDesc)
-                                )
-                            else ->
-                                WifiIcon.Visible(
-                                    WIFI_NO_INTERNET_ICONS[this.level],
-                                    ContentDescription.Loaded(
-                                        "$levelDesc,${context.getString(NO_INTERNET)}"
-                                    )
-                                )
-                        }
-                    }
+            is WifiNetworkModel.Active -> {
+                val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level])
+                when {
+                    this.isValidated ->
+                        WifiIcon.Visible(
+                            WIFI_FULL_ICONS[this.level],
+                            ContentDescription.Loaded(levelDesc),
+                        )
+                    else ->
+                        WifiIcon.Visible(
+                            WIFI_NO_INTERNET_ICONS[this.level],
+                            ContentDescription.Loaded(
+                                "$levelDesc,${context.getString(NO_INTERNET)}"
+                            ),
+                        )
                 }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
index 3e111e6..302d6a9 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
@@ -38,7 +38,7 @@
 @Inject
 constructor(
     private val inputManager: InputManager,
-    private val bluetoothAdapter: BluetoothAdapter,
+    private val bluetoothAdapter: BluetoothAdapter?,
     @Background private val handler: Handler,
     @Background private val executor: Executor,
 ) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener {
@@ -141,7 +141,7 @@
     }
 
     private fun onStylusBluetoothConnected(btAddress: String) {
-        val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(btAddress) ?: return
+        val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
         try {
             bluetoothAdapter.addOnMetadataChangedListener(device, executor, this)
         } catch (e: IllegalArgumentException) {
@@ -150,7 +150,7 @@
     }
 
     private fun onStylusBluetoothDisconnected(btAddress: String) {
-        val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(btAddress) ?: return
+        val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
         try {
             bluetoothAdapter.removeOnMetadataChangedListener(device, this)
         } catch (e: IllegalArgumentException) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
index a36105e..a4b9b08 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
@@ -22,6 +22,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -29,8 +30,12 @@
 import com.android.wm.shell.bubbles.DismissView;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 /** Tests for {@link DismissAnimationController}. */
 @SmallTest
@@ -40,10 +45,16 @@
     private DismissAnimationController mDismissAnimationController;
     private DismissView mDismissView;
 
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private AccessibilityManager mAccessibilityManager;
+
     @Before
     public void setUp() throws Exception {
         final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
-        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager);
         final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
                 stubWindowManager);
         final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
index 0cdd6e2..7356184 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -31,6 +31,7 @@
 import android.view.View;
 import android.view.ViewPropertyAnimator;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.FlingAnimation;
@@ -43,9 +44,13 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.Optional;
 
@@ -61,12 +66,18 @@
     private MenuView mMenuView;
     private TestMenuAnimationController mMenuAnimationController;
 
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private AccessibilityManager mAccessibilityManager;
+
     @Before
     public void setUp() throws Exception {
         final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
         final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
                 stubWindowManager);
-        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager);
 
         mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
         mViewPropertyAnimator = spy(mMenuView.animate());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
index e62a329..06340af 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
@@ -16,16 +16,23 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
+
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
+import android.content.res.Configuration;
 import android.testing.AndroidTestingRunner;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -34,6 +41,10 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
 /** Tests for {@link MenuInfoRepository}. */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
@@ -42,13 +53,28 @@
     public MockitoRule mockito = MockitoJUnit.rule();
 
     @Mock
+    private AccessibilityManager mAccessibilityManager;
+
+    @Mock
     private MenuInfoRepository.OnSettingsContentsChanged mMockSettingsContentsChanged;
 
     private MenuInfoRepository mMenuInfoRepository;
+    private final List<String> mShortcutTargets = new ArrayList<>();
 
     @Before
     public void setUp() {
-        mMenuInfoRepository = new MenuInfoRepository(mContext, mMockSettingsContentsChanged);
+        mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager);
+        mShortcutTargets.add(MAGNIFICATION_CONTROLLER_NAME);
+        doReturn(mShortcutTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
+                anyInt());
+
+        mMenuInfoRepository = new MenuInfoRepository(mContext, mAccessibilityManager,
+                mMockSettingsContentsChanged);
+    }
+
+    @After
+    public void tearDown() {
+        mShortcutTargets.clear();
     }
 
     @Test
@@ -64,4 +90,14 @@
 
         verify(mMockSettingsContentsChanged).onFadeEffectInfoChanged(any(MenuFadeEffectInfo.class));
     }
+
+    @Test
+    public void localeChange_verifyTargetFeaturesChanged() {
+        final Configuration configuration = new Configuration();
+        configuration.setLocale(Locale.TAIWAN);
+
+        mMenuInfoRepository.mComponentCallbacks.onConfigurationChanged(configuration);
+
+        verify(mMockSettingsContentsChanged).onTargetFeaturesChanged(any());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
index 78ee627..f17b1cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -29,6 +29,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
@@ -56,6 +57,9 @@
     public MockitoRule mockito = MockitoJUnit.rule();
 
     @Mock
+    private AccessibilityManager mAccessibilityManager;
+
+    @Mock
     private DismissAnimationController.DismissCallback mStubDismissCallback;
 
     private RecyclerView mStubListView;
@@ -69,7 +73,7 @@
         final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
         final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
                 stubWindowManager);
-        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager);
 
         final int halfScreenHeight =
                 stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
index d29ebb8..ed9562d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -31,6 +31,7 @@
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.test.filters.SmallTest;
@@ -42,8 +43,12 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -64,10 +69,16 @@
     private RecyclerView mStubListView;
     private DismissView mDismissView;
 
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private AccessibilityManager mAccessibilityManager;
+
     @Before
     public void setUp() throws Exception {
         final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager);
         final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
                 windowManager);
         mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
index 742ee53..5a1a6db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
@@ -29,6 +29,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -37,8 +38,12 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 /** Tests for {@link MenuView}. */
 @RunWith(AndroidTestingRunner.class)
@@ -52,12 +57,18 @@
     private String mLastPosition;
     private MenuViewAppearance mStubMenuViewAppearance;
 
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private AccessibilityManager mAccessibilityManager;
+
     @Before
     public void setUp() throws Exception {
         mUiModeManager = mContext.getSystemService(UiModeManager.class);
         mNightMode = mUiModeManager.getNightMode();
         mUiModeManager.setNightMode(MODE_NIGHT_YES);
-        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager);
         final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
         mStubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager);
         mMenuView = spy(new MenuView(mContext, stubMenuViewModel, mStubMenuViewAppearance));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index d6a9ee3..53cd71f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import kotlinx.coroutines.flow.MutableStateFlow
 
 // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository
@@ -29,12 +30,11 @@
     private val _connectionInfo = MutableStateFlow(MobileConnectionModel())
     override val connectionInfo = _connectionInfo
 
+    override val numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS)
+
     private val _dataEnabled = MutableStateFlow(true)
     override val dataEnabled = _dataEnabled
 
-    private val _isDefaultDataSubscription = MutableStateFlow(true)
-    override val isDefaultDataSubscription = _isDefaultDataSubscription
-
     override val cdmaRoaming = MutableStateFlow(false)
 
     override val networkName =
@@ -47,8 +47,4 @@
     fun setDataEnabled(enabled: Boolean) {
         _dataEnabled.value = enabled
     }
-
-    fun setIsDefaultDataSubscription(isDefault: Boolean) {
-        _isDefaultDataSubscription.value = isDefault
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 7f93328..49d4bdc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -57,9 +57,6 @@
     private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
     override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
 
-    private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
-    override val defaultDataSubId = _defaultDataSubId
-
     private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel())
     override val defaultMobileNetworkConnectivity = _mobileConnectivity
 
@@ -84,10 +81,6 @@
         _subscriptions.value = subs
     }
 
-    fun setDefaultDataSubId(id: Int) {
-        _defaultDataSubId.value = id
-    }
-
     fun setMobileConnectivity(model: MobileConnectivityModel) {
         _mobileConnectivity.value = model
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index c63dd2a..d6b8c0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
@@ -117,7 +118,6 @@
                 telephonyManager,
                 globalSettings,
                 fakeBroadcastDispatcher,
-                connectionsRepo.defaultDataSubId,
                 connectionsRepo.globalMobileDataSettingChangedEvent,
                 mobileMappings,
                 IMMEDIATE,
@@ -319,7 +319,7 @@
 
             val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
             val type = NETWORK_TYPE_LTE
-            val expected = DefaultNetworkType(type, mobileMappings.toIconKey(type))
+            val expected = DefaultNetworkType(mobileMappings.toIconKey(type))
             val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
             callback.onDisplayInfoChanged(ti)
 
@@ -336,7 +336,7 @@
 
             val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
             val type = OVERRIDE_NETWORK_TYPE_LTE_CA
-            val expected = OverrideNetworkType(type, mobileMappings.toIconKeyOverride(type))
+            val expected = OverrideNetworkType(mobileMappings.toIconKeyOverride(type))
             val ti =
                 mock<TelephonyDisplayInfo>().also {
                     whenever(it.networkType).thenReturn(type)
@@ -380,33 +380,6 @@
         }
 
     @Test
-    fun isDefaultDataSubscription_isDefault() =
-        runBlocking(IMMEDIATE) {
-            connectionsRepo.setDefaultDataSubId(SUB_1_ID)
-
-            var latest: Boolean? = null
-            val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this)
-
-            assertThat(latest).isTrue()
-
-            job.cancel()
-        }
-
-    @Test
-    fun isDefaultDataSubscription_isNotDefault() =
-        runBlocking(IMMEDIATE) {
-            // Our subId is SUB_1_ID
-            connectionsRepo.setDefaultDataSubId(123)
-
-            var latest: Boolean? = null
-            val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this)
-
-            assertThat(latest).isFalse()
-
-            job.cancel()
-        }
-
-    @Test
     fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() =
         runBlocking(IMMEDIATE) {
             val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID"
@@ -431,6 +404,17 @@
         }
 
     @Test
+    fun numberOfLevels_isDefault() =
+        runBlocking(IMMEDIATE) {
+            var latest: Int? = null
+            val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
+
+            job.cancel()
+        }
+
+    @Test
     fun `roaming - cdma - queries telephony manager`() =
         runBlocking(IMMEDIATE) {
             var latest: Boolean? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index b8cd7a4..0da15e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -307,35 +307,6 @@
         }
 
     @Test
-    fun testDefaultDataSubId_updatesOnBroadcast() =
-        runBlocking(IMMEDIATE) {
-            var latest: Int? = null
-            val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this)
-
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(
-                    context,
-                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
-                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
-                )
-            }
-
-            assertThat(latest).isEqualTo(SUB_2_ID)
-
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(
-                    context,
-                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
-                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
-                )
-            }
-
-            assertThat(latest).isEqualTo(SUB_1_ID)
-
-            job.cancel()
-        }
-
-    @Test
     fun mobileConnectivity_default() {
         assertThat(underTest.defaultMobileNetworkConnectivity.value)
             .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index ff72715..a29146b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -21,6 +21,7 @@
 import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -65,7 +66,7 @@
     private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
     override val level = _level
 
-    private val _numberOfLevels = MutableStateFlow(4)
+    private val _numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS)
     override val numberOfLevels = _numberOfLevels
 
     fun setIconGroup(group: SignalIcon.MobileIconGroup) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 5abe335..61e13b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
 import android.telephony.CellSignalStrength
-import android.telephony.SubscriptionInfo
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
 import com.android.settingslib.SignalIcon.MobileIconGroup
@@ -34,7 +33,6 @@
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -178,12 +176,26 @@
         }
 
     @Test
+    fun numberOfLevels_comesFromRepo() =
+        runBlocking(IMMEDIATE) {
+            var latest: Int? = null
+            val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.numberOfLevels.value = 5
+            assertThat(latest).isEqualTo(5)
+
+            connectionRepository.numberOfLevels.value = 4
+            assertThat(latest).isEqualTo(4)
+
+            job.cancel()
+        }
+
+    @Test
     fun iconGroup_three_g() =
         runBlocking(IMMEDIATE) {
             connectionRepository.setConnectionInfo(
                 MobileConnectionModel(
-                    resolvedNetworkType =
-                        DefaultNetworkType(THREE_G, mobileMappingsProxy.toIconKey(THREE_G))
+                    resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
                 ),
             )
 
@@ -200,8 +212,7 @@
         runBlocking(IMMEDIATE) {
             connectionRepository.setConnectionInfo(
                 MobileConnectionModel(
-                    resolvedNetworkType =
-                        DefaultNetworkType(THREE_G, mobileMappingsProxy.toIconKey(THREE_G))
+                    resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
                 ),
             )
 
@@ -212,7 +223,6 @@
                 MobileConnectionModel(
                     resolvedNetworkType =
                         DefaultNetworkType(
-                            FOUR_G,
                             mobileMappingsProxy.toIconKey(FOUR_G),
                         ),
                 ),
@@ -230,10 +240,7 @@
             connectionRepository.setConnectionInfo(
                 MobileConnectionModel(
                     resolvedNetworkType =
-                        OverrideNetworkType(
-                            FIVE_G_OVERRIDE,
-                            mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)
-                        )
+                        OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE))
                 ),
             )
 
@@ -251,10 +258,7 @@
             connectionRepository.setConnectionInfo(
                 MobileConnectionModel(
                     resolvedNetworkType =
-                        DefaultNetworkType(
-                            NETWORK_TYPE_UNKNOWN,
-                            mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)
-                        ),
+                        DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)),
                 ),
             )
 
@@ -509,8 +513,6 @@
         private const val CDMA_LEVEL = 2
 
         private const val SUB_1_ID = 1
-        private val SUB_1 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
 
         private val DEFAULT_NAME = NetworkNameModel.Default("test default name")
         private val DERIVED_NAME = NetworkNameModel.Derived("test derived name")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
new file mode 100644
index 0000000..a2c1209
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.view
+
+import android.content.res.ColorStateList
+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
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class ModernStatusBarMobileViewTest : SysuiTestCase() {
+
+    private lateinit var testableLooper: TestableLooper
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
+    @Mock private lateinit var tableLogBuffer: TableLogBuffer
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var constants: ConnectivityConstants
+
+    private lateinit var viewModel: LocationBasedMobileViewModel
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
+
+        val interactor = FakeMobileIconInteractor(tableLogBuffer)
+
+        val viewModelCommon =
+            MobileIconViewModel(
+                subscriptionId = 1,
+                interactor,
+                logger,
+                constants,
+                testScope.backgroundScope,
+            )
+        viewModel = QsMobileIconViewModel(viewModelCommon, statusBarPipelineFlags)
+    }
+
+    // Note: The following tests are more like integration tests, since they stand up a full
+    // [WifiViewModel] and test the interactions between the view, view-binder, and view-model.
+
+    @Test
+    fun setVisibleState_icon_iconShownDotHidden() {
+        val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
+
+        view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false)
+
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        assertThat(view.getGroupView().visibility).isEqualTo(View.VISIBLE)
+        assertThat(view.getDotView().visibility).isEqualTo(View.GONE)
+
+        ViewUtils.detachView(view)
+    }
+
+    @Test
+    fun setVisibleState_dot_iconHiddenDotShown() {
+        val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
+
+        view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false)
+
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        assertThat(view.getGroupView().visibility).isEqualTo(View.INVISIBLE)
+        assertThat(view.getDotView().visibility).isEqualTo(View.VISIBLE)
+
+        ViewUtils.detachView(view)
+    }
+
+    @Test
+    fun setVisibleState_hidden_iconAndDotHidden() {
+        val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
+
+        view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false)
+
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        assertThat(view.getGroupView().visibility).isEqualTo(View.INVISIBLE)
+        assertThat(view.getDotView().visibility).isEqualTo(View.INVISIBLE)
+
+        ViewUtils.detachView(view)
+    }
+
+    @Test
+    fun isIconVisible_alwaysTrue() {
+        val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
+
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        assertThat(view.isIconVisible).isTrue()
+
+        ViewUtils.detachView(view)
+    }
+
+    @Test
+    fun onDarkChanged_iconHasNewColor() {
+        whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
+        val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        val color = 0x12345678
+        view.onDarkChanged(arrayListOf(), 1.0f, color)
+        testableLooper.processAllMessages()
+
+        assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
+
+        ViewUtils.detachView(view)
+    }
+
+    @Test
+    fun setStaticDrawableColor_iconHasNewColor() {
+        whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
+        val view = ModernStatusBarMobileView.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))
+
+        ViewUtils.detachView(view)
+    }
+
+    private fun View.getGroupView(): View {
+        return this.requireViewById(R.id.mobile_group)
+    }
+
+    private fun View.getIconView(): ImageView {
+        return this.requireViewById(R.id.mobile_signal)
+    }
+
+    private fun View.getDotView(): View {
+        return this.requireViewById(R.id.status_bar_dot)
+    }
+}
+
+private const val SLOT_NAME = "TestSlotName"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
index 043d55a..c960a06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
@@ -20,6 +20,7 @@
 import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
@@ -45,6 +46,7 @@
     private lateinit var qsIcon: QsMobileIconViewModel
     private lateinit var keyguardIcon: KeyguardMobileIconViewModel
     private lateinit var interactor: FakeMobileIconInteractor
+    @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var constants: ConnectivityConstants
     @Mock private lateinit var tableLogBuffer: TableLogBuffer
@@ -68,9 +70,9 @@
         commonImpl =
             MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
 
-        homeIcon = HomeMobileIconViewModel(commonImpl, logger)
-        qsIcon = QsMobileIconViewModel(commonImpl, logger)
-        keyguardIcon = KeyguardMobileIconViewModel(commonImpl, logger)
+        homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags)
+        qsIcon = QsMobileIconViewModel(commonImpl, statusBarPipelineFlags)
+        keyguardIcon = KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
index d6cb762..58b50c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
@@ -45,6 +46,7 @@
     private lateinit var underTest: MobileIconsViewModel
     private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
 
+    @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var constants: ConnectivityConstants
 
@@ -67,6 +69,7 @@
                 logger,
                 constants,
                 testScope.backgroundScope,
+                statusBarPipelineFlags,
             )
 
         interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
new file mode 100644
index 0000000..3fe6983
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.view
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
+import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+class ModernStatusBarViewTest : SysuiTestCase() {
+
+    private lateinit var binding: TestBinding
+
+    @Test
+    fun initView_hasCorrectSlot() {
+        val view = ModernStatusBarView(context, null)
+        val binding = TestBinding()
+
+        view.initView("slotName") { binding }
+
+        assertThat(view.slot).isEqualTo("slotName")
+    }
+
+    @Test
+    fun getVisibleState_icon_returnsIcon() {
+        val view = createAndInitView()
+
+        view.setVisibleState(STATE_ICON, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(STATE_ICON)
+    }
+
+    @Test
+    fun getVisibleState_dot_returnsDot() {
+        val view = createAndInitView()
+
+        view.setVisibleState(STATE_DOT, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(STATE_DOT)
+    }
+
+    @Test
+    fun getVisibleState_hidden_returnsHidden() {
+        val view = createAndInitView()
+
+        view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(STATE_HIDDEN)
+    }
+
+    @Test
+    fun onDarkChanged_bindingReceivesIconAndDecorTint() {
+        val view = createAndInitView()
+
+        view.onDarkChanged(arrayListOf(), 1.0f, 0x12345678)
+
+        assertThat(binding.iconTint).isEqualTo(0x12345678)
+        assertThat(binding.decorTint).isEqualTo(0x12345678)
+    }
+
+    @Test
+    fun setStaticDrawableColor_bindingReceivesIconTint() {
+        val view = createAndInitView()
+
+        view.setStaticDrawableColor(0x12345678)
+
+        assertThat(binding.iconTint).isEqualTo(0x12345678)
+    }
+
+    @Test
+    fun setDecorColor_bindingReceivesDecorColor() {
+        val view = createAndInitView()
+
+        view.setDecorColor(0x23456789)
+
+        assertThat(binding.decorTint).isEqualTo(0x23456789)
+    }
+
+    @Test
+    fun isIconVisible_usesBinding_true() {
+        val view = createAndInitView()
+
+        binding.shouldIconBeVisibleInternal = true
+
+        assertThat(view.isIconVisible).isEqualTo(true)
+    }
+
+    @Test
+    fun isIconVisible_usesBinding_false() {
+        val view = createAndInitView()
+
+        binding.shouldIconBeVisibleInternal = false
+
+        assertThat(view.isIconVisible).isEqualTo(false)
+    }
+
+    private fun createAndInitView(): ModernStatusBarView {
+        val view = ModernStatusBarView(context, null)
+        binding = TestBinding()
+        view.initView(SLOT_NAME) { binding }
+        return view
+    }
+
+    inner class TestBinding : ModernStatusBarViewBinding {
+        var iconTint: Int? = null
+        var decorTint: Int? = null
+        var onVisibilityStateChangedCalled: Boolean = false
+
+        var shouldIconBeVisibleInternal: Boolean = true
+
+        override fun onIconTintChanged(newTint: Int) {
+            iconTint = newTint
+        }
+
+        override fun onDecorTintChanged(newTint: Int) {
+            decorTint = newTint
+        }
+
+        override fun onVisibilityStateChanged(state: Int) {
+            onVisibilityStateChangedCalled = true
+        }
+
+        override fun getShouldIconBeVisible(): Boolean {
+            return shouldIconBeVisibleInternal
+        }
+    }
+}
+
+private const val SLOT_NAME = "TestSlotName"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
index 30fd308..30ac8d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
@@ -34,12 +34,6 @@
         }
     }
 
-    @Test
-    fun active_levelNull_noException() {
-        WifiNetworkModel.Active(NETWORK_ID, level = null)
-        // No assert, just need no crash
-    }
-
     @Test(expected = IllegalArgumentException::class)
     fun active_levelNegative_exceptionThrown() {
         WifiNetworkModel.Active(NETWORK_ID, level = MIN_VALID_LEVEL - 1)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
new file mode 100644
index 0000000..3c4e85b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class DisabledWifiRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: DisabledWifiRepository
+
+    @Before
+    fun setUp() {
+        underTest = DisabledWifiRepository()
+    }
+
+    @Test
+    fun enabled_alwaysFalse() {
+        assertThat(underTest.isWifiEnabled.value).isEqualTo(false)
+    }
+
+    @Test
+    fun default_alwaysFalse() {
+        assertThat(underTest.isWifiDefault.value).isEqualTo(false)
+    }
+
+    @Test
+    fun network_alwaysUnavailable() {
+        assertThat(underTest.wifiNetwork.value).isEqualTo(WifiNetworkModel.Unavailable)
+    }
+
+    @Test
+    fun activity_alwaysFalse() {
+        assertThat(underTest.wifiActivity.value)
+            .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index befb290..8f07615 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
@@ -98,13 +97,6 @@
     }
 
     @Test
-    fun isWifiEnabled_nullWifiManager_getsFalse() = runBlocking(IMMEDIATE) {
-        underTest = createRepo(wifiManagerToUse = null)
-
-        assertThat(underTest.isWifiEnabled.value).isFalse()
-    }
-
-    @Test
     fun isWifiEnabled_initiallyGetsWifiManagerValue() = runBlocking(IMMEDIATE) {
         whenever(wifiManager.isWifiEnabled).thenReturn(true)
 
@@ -721,21 +713,6 @@
     }
 
     @Test
-    fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
-        underTest = createRepo(wifiManagerToUse = null)
-
-        var latest: DataActivityModel? = null
-        val job = underTest
-                .wifiActivity
-                .onEach { latest = it }
-                .launchIn(this)
-
-        assertThat(latest).isEqualTo(ACTIVITY_DEFAULT)
-
-        job.cancel()
-    }
-
-    @Test
     fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) {
         var latest: DataActivityModel? = null
         val job = underTest
@@ -801,7 +778,7 @@
         job.cancel()
     }
 
-    private fun createRepo(wifiManagerToUse: WifiManager? = wifiManager): WifiRepositoryImpl {
+    private fun createRepo(): WifiRepositoryImpl {
         return WifiRepositoryImpl(
             broadcastDispatcher,
             connectivityManager,
@@ -809,7 +786,7 @@
             tableLogger,
             executor,
             scope,
-            wifiManagerToUse,
+            wifiManager,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index 2ecb17b..01d59f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -52,6 +52,22 @@
     }
 
     @Test
+    fun ssid_unavailableNetwork_outputsNull() =
+        runBlocking(IMMEDIATE) {
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Unavailable)
+
+            var latest: String? = "default"
+            val job = underTest
+                .ssid
+                .onEach { latest = it }
+                .launchIn(this)
+
+            assertThat(latest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
     fun ssid_inactiveNetwork_outputsNull() = runBlocking(IMMEDIATE) {
         wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
 
@@ -85,6 +101,7 @@
     fun ssid_isPasspointAccessPoint_outputsPasspointName() = runBlocking(IMMEDIATE) {
         wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
             networkId = 1,
+            level = 1,
             isPasspointAccessPoint = true,
             passpointProviderFriendlyName = "friendly",
         ))
@@ -104,6 +121,7 @@
     fun ssid_isOnlineSignUpForPasspoint_outputsPasspointName() = runBlocking(IMMEDIATE) {
         wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
             networkId = 1,
+            level = 1,
             isOnlineSignUpForPasspointAccessPoint = true,
             passpointProviderFriendlyName = "friendly",
         ))
@@ -123,6 +141,7 @@
     fun ssid_unknownSsid_outputsNull() = runBlocking(IMMEDIATE) {
         wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
             networkId = 1,
+            level = 1,
             ssid = WifiManager.UNKNOWN_SSID,
         ))
 
@@ -141,6 +160,7 @@
     fun ssid_validSsid_outputsSsid() = runBlocking(IMMEDIATE) {
         wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
             networkId = 1,
+            level = 1,
             ssid = "MyAwesomeWifiNetwork",
         ))
 
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 59c10cd..b8ace2f 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
@@ -17,7 +17,6 @@
 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
@@ -27,7 +26,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.lifecycle.InstantTaskExecutorRule
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
@@ -52,8 +50,6 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import org.junit.Before
-import org.junit.Ignore
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
@@ -70,7 +66,8 @@
     private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock
     private lateinit var logger: ConnectivityPipelineLogger
-    @Mock private lateinit var tableLogBuffer: TableLogBuffer
+    @Mock
+    private lateinit var tableLogBuffer: TableLogBuffer
     @Mock
     private lateinit var connectivityConstants: ConnectivityConstants
     @Mock
@@ -83,9 +80,6 @@
     private lateinit var scope: CoroutineScope
     private lateinit var airplaneModeViewModel: AirplaneModeViewModel
 
-    @JvmField @Rule
-    val instantTaskExecutor = InstantTaskExecutorRule()
-
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -118,40 +112,6 @@
         ).home
     }
 
-    @Test
-    fun constructAndBind_hasCorrectSlot() {
-        val view = ModernStatusBarWifiView.constructAndBind(context, "slotName", viewModel)
-
-        assertThat(view.slot).isEqualTo("slotName")
-    }
-
-    @Test
-    fun getVisibleState_icon_returnsIcon() {
-        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
-
-        view.setVisibleState(STATE_ICON, /* animate= */ false)
-
-        assertThat(view.visibleState).isEqualTo(STATE_ICON)
-    }
-
-    @Test
-    fun getVisibleState_dot_returnsDot() {
-        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
-
-        view.setVisibleState(STATE_DOT, /* animate= */ false)
-
-        assertThat(view.visibleState).isEqualTo(STATE_DOT)
-    }
-
-    @Test
-    fun getVisibleState_hidden_returnsHidden() {
-        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
-
-        view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
-
-        assertThat(view.visibleState).isEqualTo(STATE_HIDDEN)
-    }
-
     // Note: The following tests are more like integration tests, since they stand up a full
     // [WifiViewModel] and test the interactions between the view, view-binder, and view-model.
 
@@ -235,24 +195,24 @@
     }
 
     @Test
-    @Ignore("b/262660044")
     fun onDarkChanged_iconHasNewColor() {
-        whenever(statusBarPipelineFlags.useWifiDebugColoring()).thenReturn(false)
+        whenever(statusBarPipelineFlags.useDebugColoring()).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)
+        view.onDarkChanged(arrayListOf(), 1.0f, color)
         testableLooper.processAllMessages()
 
         assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
+
+        ViewUtils.detachView(view)
     }
 
     @Test
     fun setStaticDrawableColor_iconHasNewColor() {
-        whenever(statusBarPipelineFlags.useWifiDebugColoring()).thenReturn(false)
+        whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
         ViewUtils.attachView(view)
         testableLooper.processAllMessages()
@@ -262,6 +222,8 @@
         testableLooper.processAllMessages()
 
         assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
+
+        ViewUtils.detachView(view)
     }
 
     private fun View.getIconGroupView(): View {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 12b93819..726e813 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -379,6 +379,12 @@
                     expected = null,
                 ),
 
+                // network = Unavailable => not shown
+                TestCase(
+                    network = WifiNetworkModel.Unavailable,
+                    expected = null,
+                ),
+
                 // network = Active & validated = false => not shown
                 TestCase(
                     network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3),
@@ -397,12 +403,6 @@
                             description = "Full internet level 4 icon",
                         ),
                 ),
-
-                // network has null level => not shown
-                TestCase(
-                    network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = null),
-                    expected = null,
-                ),
             )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 4158434..e5cfec9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -228,7 +228,7 @@
         whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
         createAndSetViewModel()
 
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null))
+        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null, level = 1))
 
         var activityIn: Boolean? = null
         val activityInJob = underTest
@@ -553,7 +553,8 @@
 
     companion object {
         private const val NETWORK_ID = 2
-        private val ACTIVE_VALID_WIFI_NETWORK = WifiNetworkModel.Active(NETWORK_ID, ssid = "AB")
+        private val ACTIVE_VALID_WIFI_NETWORK =
+            WifiNetworkModel.Active(NETWORK_ID, ssid = "AB", level = 1)
     }
 }
 
diff --git a/services/api/current.txt b/services/api/current.txt
index 090a449..b173726 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -40,6 +40,7 @@
   public interface ActivityManagerLocal {
     method public boolean bindSdkSandboxService(@NonNull android.content.Intent, @NonNull android.content.ServiceConnection, int, @NonNull String, @NonNull String, int) throws android.os.RemoteException;
     method public boolean canStartForegroundService(int, int, @NonNull String);
+    method public void killSdkSandboxClientAppProcess(@NonNull android.os.IBinder);
   }
 
 }
@@ -212,6 +213,19 @@
 
 }
 
+package com.android.server.security {
+
+  public class FileIntegrityService extends com.android.server.SystemService {
+    method public void onStart();
+    method public static void setUpFsVerity(@NonNull String) throws java.io.IOException;
+  }
+
+  public class KeyChainSystemService extends com.android.server.SystemService {
+    method public void onStart();
+  }
+
+}
+
 package com.android.server.stats {
 
   public final class StatsHelper {
diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
index 3ff6ba7..38c7dd1 100644
--- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java
+++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
@@ -375,7 +375,7 @@
 
             case MSG_RUN_GET_RESTORE_SETS: {
                 // Like other async operations, this is entered with the wakelock held
-                RestoreSet[] sets = null;
+                List<RestoreSet> sets = null;
                 RestoreGetSetsParams params = (RestoreGetSetsParams) msg.obj;
                 String callerLogString = "BH/MSG_RUN_GET_RESTORE_SETS";
                 try {
@@ -394,7 +394,12 @@
                 } finally {
                     if (params.observer != null) {
                         try {
-                            params.observer.restoreSetsAvailable(sets);
+                            if (sets == null) {
+                                params.observer.restoreSetsAvailable(null);
+                            } else {
+                                params.observer.restoreSetsAvailable(
+                                        sets.toArray(new RestoreSet[0]));
+                            }
                         } catch (RemoteException re) {
                             Slog.e(TAG, "Unable to report listing to observer");
                         } catch (Exception e) {
diff --git a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
index d3e4f13..70d7fac 100644
--- a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
+++ b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
@@ -45,6 +45,7 @@
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.utils.BackupEligibilityRules;
 
+import java.util.List;
 import java.util.function.BiFunction;
 
 /**
@@ -60,7 +61,7 @@
     private final int mUserId;
     private final BackupEligibilityRules mBackupEligibilityRules;
     @Nullable private final String mPackageName;
-    public RestoreSet[] mRestoreSets = null;
+    public List<RestoreSet> mRestoreSets = null;
     boolean mEnded = false;
     boolean mTimedOut = false;
 
@@ -174,10 +175,10 @@
         }
 
         synchronized (mBackupManagerService.getQueueLock()) {
-            for (int i = 0; i < mRestoreSets.length; i++) {
-                if (token == mRestoreSets[i].token) {
+            for (int i = 0; i < mRestoreSets.size(); i++) {
+                if (token == mRestoreSets.get(i).token) {
                     final long oldId = Binder.clearCallingIdentity();
-                    RestoreSet restoreSet = mRestoreSets[i];
+                    RestoreSet restoreSet = mRestoreSets.get(i);
                     try {
                         return sendRestoreToHandlerLocked(
                                 (transportClient, listener) ->
@@ -267,10 +268,10 @@
         }
 
         synchronized (mBackupManagerService.getQueueLock()) {
-            for (int i = 0; i < mRestoreSets.length; i++) {
-                if (token == mRestoreSets[i].token) {
+            for (int i = 0; i < mRestoreSets.size(); i++) {
+                if (token == mRestoreSets.get(i).token) {
                     final long oldId = Binder.clearCallingIdentity();
-                    RestoreSet restoreSet = mRestoreSets[i];
+                    RestoreSet restoreSet = mRestoreSets.get(i);
                     try {
                         return sendRestoreToHandlerLocked(
                                 (transportClient, listener) ->
@@ -390,7 +391,7 @@
         }
     }
 
-    public void setRestoreSets(RestoreSet[] restoreSets) {
+    public void setRestoreSets(List<RestoreSet> restoreSets) {
         mRestoreSets = restoreSets;
     }
 
diff --git a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
index 21005bb..daf34152 100644
--- a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
+++ b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
@@ -180,11 +180,11 @@
     /**
      * See {@link IBackupTransport#getAvailableRestoreSets()}
      */
-    public RestoreSet[] getAvailableRestoreSets() throws RemoteException {
+    public List<RestoreSet> getAvailableRestoreSets() throws RemoteException {
         AndroidFuture<List<RestoreSet>> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.getAvailableRestoreSets(resultFuture);
         List<RestoreSet> result = getFutureResult(resultFuture);
-        return result == null ? null : result.toArray(new RestoreSet[] {});
+        return result;
     }
 
     /**
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 373080a..ff6fd4b 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -273,7 +273,7 @@
                     String[] signerDigestHexStrings = computePackageSignerSha256Digests(
                             packageInfo.signingInfo);
 
-                    // log to Westworld
+                    // log to statsd
                     FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED,
                                             packageInfo.packageName,
                                             packageInfo.getLongVersionCode(),
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 7b8ca91..9bedbd0 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1000,10 +1000,6 @@
     @Override
     public void notifySubscriptionInfoChanged() {
         if (VDBG) log("notifySubscriptionInfoChanged:");
-        if (!checkNotifyPermission("notifySubscriptionInfoChanged()")) {
-            return;
-        }
-
         synchronized (mRecords) {
             if (!mHasNotifySubscriptionInfoChangedOccurred) {
                 log("notifySubscriptionInfoChanged: first invocation mRecords.size="
@@ -1030,10 +1026,6 @@
     @Override
     public void notifyOpportunisticSubscriptionInfoChanged() {
         if (VDBG) log("notifyOpptSubscriptionInfoChanged:");
-        if (!checkNotifyPermission("notifyOpportunisticSubscriptionInfoChanged()")) {
-            return;
-        }
-
         synchronized (mRecords) {
             if (!mHasNotifyOpportunisticSubscriptionInfoChangedOccurred) {
                 log("notifyOpptSubscriptionInfoChanged: first invocation mRecords.size="
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 4ebd714..bcea40e5 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -54,6 +54,7 @@
 import static android.os.PowerExemptionManager.REASON_OPT_OUT_REQUESTED;
 import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_PLATFORM_VPN;
 import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_VPN;
+import static android.os.PowerExemptionManager.REASON_PACKAGE_INSTALLER;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_TOP;
@@ -194,6 +195,7 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.os.TimeoutRecord;
 import com.android.internal.os.TransferPipe;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.FrameworkStatsLog;
@@ -202,6 +204,7 @@
 import com.android.server.SystemService;
 import com.android.server.am.ActivityManagerService.ItemMatcher;
 import com.android.server.am.LowMemDetector.MemFactor;
+import com.android.server.pm.KnownPackages;
 import com.android.server.uri.NeededUriGrants;
 import com.android.server.wm.ActivityServiceConnectionsHolder;
 
@@ -2382,6 +2385,13 @@
                             .getPotentialUserAllowedExemptionReason(callerUid, packageName);
                 }
             }
+            if (reason == REASON_DENIED) {
+                if (ArrayUtils.contains(mAm.getPackageManagerInternal().getKnownPackageNames(
+                        KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM), packageName)) {
+                    reason = REASON_PACKAGE_INSTALLER;
+                }
+            }
+
             switch (reason) {
                 case REASON_SYSTEM_UID:
                 case REASON_SYSTEM_ALLOW_LISTED:
@@ -2397,6 +2407,7 @@
                 case REASON_ACTIVE_DEVICE_ADMIN:
                 case REASON_ROLE_EMERGENCY:
                 case REASON_ALLOWLISTED_PACKAGE:
+                case REASON_PACKAGE_INSTALLER:
                     return PERMISSION_GRANTED;
                 default:
                     return PERMISSION_DENIED;
diff --git a/services/core/java/com/android/server/am/ActivityManagerLocal.java b/services/core/java/com/android/server/am/ActivityManagerLocal.java
index 9f2cc7f..5175a31 100644
--- a/services/core/java/com/android/server/am/ActivityManagerLocal.java
+++ b/services/core/java/com/android/server/am/ActivityManagerLocal.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.os.IBinder;
 import android.os.RemoteException;
 
 /**
@@ -95,6 +96,15 @@
             throws RemoteException;
 
     /**
+     * Kill an app process associated with an SDK sandbox.
+     *
+     * @param clientApplicationThreadBinder binder value of the
+     *        {@link android.app.IApplicationThread} of a client app process associated with a
+     *        sandbox. This is obtained using {@link Context#getIApplicationThreadBinder()}.
+     */
+    void killSdkSandboxClientAppProcess(@NonNull IBinder clientApplicationThreadBinder);
+
+    /**
      * Start a foreground service delegate.
      * @param options foreground service delegate options.
      * @param connection a service connection served as callback to caller.
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 191460c..28aed2e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -13953,7 +13953,7 @@
                     }
                 } else {
                     BroadcastFilter bf = (BroadcastFilter)target;
-                    if (bf.requiredPermission == null) {
+                    if (bf.exported && bf.requiredPermission == null) {
                         allProtected = false;
                         break;
                     }
@@ -16958,6 +16958,20 @@
         }
 
         @Override
+        public void killSdkSandboxClientAppProcess(IBinder clientApplicationThreadBinder) {
+            synchronized (ActivityManagerService.this) {
+                ProcessRecord r = getRecordForAppLOSP(clientApplicationThreadBinder);
+                if (r != null) {
+                    r.killLocked(
+                            "sdk sandbox died",
+                            ApplicationExitInfo.REASON_DEPENDENCY_DIED,
+                            ApplicationExitInfo.SUBREASON_SDK_SANDBOX_DIED,
+                            true);
+                }
+            }
+        }
+
+        @Override
         public void onUserRemoved(@UserIdInt int userId) {
             // Clean up any ActivityTaskManager state (by telling it the user is stopped)
             mAtmInternal.onUserStopped(userId);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0f7a725..78bff95 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -106,6 +106,7 @@
 import android.media.ICapturePresetDevicesRoleDispatcher;
 import android.media.ICommunicationDeviceDispatcher;
 import android.media.IDeviceVolumeBehaviorDispatcher;
+import android.media.IDevicesForAttributesCallback;
 import android.media.IMuteAwaitConnectionCallback;
 import android.media.IPlaybackConfigDispatcher;
 import android.media.IPreferredMixerAttributesDispatcher;
@@ -3124,6 +3125,25 @@
         return mAudioSystem.getDevicesForAttributes(attributes, forVolume);
     }
 
+    /**
+     * @see AudioManager#addOnDevicesForAttributesChangedListener(
+     *      AudioAttributes, Executor, OnDevicesForAttributesChangedListener)
+     */
+    public void addOnDevicesForAttributesChangedListener(AudioAttributes attributes,
+            IDevicesForAttributesCallback callback) {
+        mAudioSystem.addOnDevicesForAttributesChangedListener(
+                attributes, false /* forVolume */, callback);
+    }
+
+    /**
+     * @see AudioManager#removeOnDevicesForAttributesChangedListener(
+     *      OnDevicesForAttributesChangedListener)
+     */
+    public void removeOnDevicesForAttributesChangedListener(
+            IDevicesForAttributesCallback callback) {
+        mAudioSystem.removeOnDevicesForAttributesChangedListener(callback);
+    }
+
     // pre-condition: event.getKeyCode() is one of KeyEvent.KEYCODE_VOLUME_UP,
     //                                   KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE
     public void handleVolumeKey(@NonNull KeyEvent event, boolean isOnTv,
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 7fefc55..7839ada 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -22,10 +22,15 @@
 import android.media.AudioDeviceAttributes;
 import android.media.AudioMixerAttributes;
 import android.media.AudioSystem;
+import android.media.IDevicesForAttributesCallback;
 import android.media.ISoundDose;
 import android.media.ISoundDoseCallback;
 import android.media.audiopolicy.AudioMix;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
 import android.os.SystemClock;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
 
@@ -64,8 +69,21 @@
 
     private static final boolean USE_CACHE_FOR_GETDEVICES = true;
     private ConcurrentHashMap<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
+            mLastDevicesForAttr = new ConcurrentHashMap<>();
+    private ConcurrentHashMap<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
             mDevicesForAttrCache;
+    private final Object mDeviceCacheLock = new Object();
     private int[] mMethodCacheHit;
+    /**
+     * Map that stores all attributes + forVolume pairs that are registered for
+     * routing change callback. The key is the {@link IBinder} that corresponds
+     * to the remote callback.
+     */
+    private final ArrayMap<IBinder, List<Pair<AudioAttributes, Boolean>>> mRegisteredAttributesMap =
+            new ArrayMap<>();
+    private final RemoteCallbackList<IDevicesForAttributesCallback>
+            mDevicesForAttributesCallbacks = new RemoteCallbackList<>();
+
     private static final Object sRoutingListenerLock = new Object();
     @GuardedBy("sRoutingListenerLock")
     private static @Nullable OnRoutingUpdatedListener sRoutingListener;
@@ -96,6 +114,34 @@
         if (listener != null) {
             listener.onRoutingUpdatedFromNative();
         }
+
+        synchronized (mRegisteredAttributesMap) {
+            final int nbCallbacks = mDevicesForAttributesCallbacks.beginBroadcast();
+
+            for (int i = 0; i < nbCallbacks; i++) {
+                IDevicesForAttributesCallback cb =
+                        mDevicesForAttributesCallbacks.getBroadcastItem(i);
+                List<Pair<AudioAttributes, Boolean>> attrList =
+                        mRegisteredAttributesMap.get(cb.asBinder());
+
+                if (attrList == null) {
+                    throw new IllegalStateException("Attribute list must not be null");
+                }
+
+                for (Pair<AudioAttributes, Boolean> attr : attrList) {
+                    ArrayList<AudioDeviceAttributes> devices =
+                            getDevicesForAttributes(attr.first, attr.second);
+                    if (!mLastDevicesForAttr.containsKey(attr)
+                            || !sameDeviceList(devices, mLastDevicesForAttr.get(attr))) {
+                        try {
+                            cb.onDevicesForAttributesChanged(
+                                    attr.first, attr.second, devices);
+                        } catch (RemoteException e) { }
+                    }
+                }
+            }
+            mDevicesForAttributesCallbacks.finishBroadcast();
+        }
     }
 
     interface OnRoutingUpdatedListener {
@@ -109,6 +155,66 @@
     }
 
     /**
+     * @see AudioManager#addOnDevicesForAttributesChangedListener(
+     *      AudioAttributes, Executor, OnDevicesForAttributesChangedListener)
+     */
+    public void addOnDevicesForAttributesChangedListener(AudioAttributes attributes,
+            boolean forVolume, @NonNull IDevicesForAttributesCallback listener) {
+        List<Pair<AudioAttributes, Boolean>> res;
+        final Pair<AudioAttributes, Boolean> attr = new Pair(attributes, forVolume);
+        synchronized (mRegisteredAttributesMap) {
+            res = mRegisteredAttributesMap.get(listener.asBinder());
+            if (res == null) {
+                res = new ArrayList<>();
+                mRegisteredAttributesMap.put(listener.asBinder(), res);
+                mDevicesForAttributesCallbacks.register(listener);
+            }
+
+            if (!res.contains(attr)) {
+                res.add(attr);
+            }
+        }
+
+        // Make query on registration to populate cache
+        getDevicesForAttributes(attributes, forVolume);
+    }
+
+    /**
+     * @see AudioManager#removeOnDevicesForAttributesChangedListener(
+     *      OnDevicesForAttributesChangedListener)
+     */
+    public void removeOnDevicesForAttributesChangedListener(
+            @NonNull IDevicesForAttributesCallback listener) {
+        synchronized (mRegisteredAttributesMap) {
+            if (!mRegisteredAttributesMap.containsKey(listener.asBinder())) {
+                Log.w(TAG, "listener to be removed is not found.");
+                return;
+            }
+            mRegisteredAttributesMap.remove(listener.asBinder());
+            mDevicesForAttributesCallbacks.unregister(listener);
+        }
+    }
+
+    /**
+     * Helper function to compare lists of {@link AudioDeviceAttributes}
+     * @return true if the two lists contains the same devices, false otherwise.
+     */
+    private boolean sameDeviceList(@NonNull List<AudioDeviceAttributes> a,
+            @NonNull List<AudioDeviceAttributes> b) {
+        for (AudioDeviceAttributes device : a) {
+            if (!b.contains(device)) {
+                return false;
+            }
+        }
+        for (AudioDeviceAttributes device : b) {
+            if (!a.contains(device)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
      * Implementation of AudioSystem.VolumeRangeInitRequestCallback
      */
     @Override
@@ -159,8 +265,11 @@
         if (DEBUG_CACHE) {
             Log.d(TAG, "---- clearing cache ----------");
         }
-        if (mDevicesForAttrCache != null) {
-            synchronized (mDevicesForAttrCache) {
+        synchronized (mDeviceCacheLock) {
+            if (mDevicesForAttrCache != null) {
+                // Save latest cache to determine routing updates
+                mLastDevicesForAttr.putAll(mDevicesForAttrCache);
+
                 mDevicesForAttrCache.clear();
             }
         }
@@ -189,7 +298,7 @@
         if (USE_CACHE_FOR_GETDEVICES) {
             ArrayList<AudioDeviceAttributes> res;
             final Pair<AudioAttributes, Boolean> key = new Pair(attributes, forVolume);
-            synchronized (mDevicesForAttrCache) {
+            synchronized (mDeviceCacheLock) {
                 res = mDevicesForAttrCache.get(key);
                 if (res == null) {
                     res = AudioSystem.getDevicesForAttributes(attributes, forVolume);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index f4eed2b..affeef1 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -852,6 +852,10 @@
                 mAutomaticBrightnessController.stop();
             }
 
+            if (mScreenOffBrightnessSensorController != null) {
+                mScreenOffBrightnessSensorController.stop();
+            }
+
             if (mBrightnessSetting != null) {
                 mBrightnessSetting.unregisterListener(mBrightnessSettingListener);
             }
@@ -1093,6 +1097,9 @@
             mBrightnessEventRingBuffer =
                     new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
 
+            if (mScreenOffBrightnessSensorController != null) {
+                mScreenOffBrightnessSensorController.stop();
+            }
             loadScreenOffBrightnessSensor();
             int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux();
             if (mScreenOffBrightnessSensor != null && sensorValueToLux != null) {
diff --git a/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java
index 6f50dac..4d394c2 100644
--- a/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java
+++ b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java
@@ -92,6 +92,10 @@
         }
     }
 
+    void stop() {
+        setLightSensorEnabled(false);
+    }
+
     float getAutomaticScreenBrightness() {
         if (mLastSensorValue < 0 || mLastSensorValue >= mSensorValueToLux.length
                 || (!mRegistered
diff --git a/services/core/java/com/android/server/grammaticalinflection/OWNERS b/services/core/java/com/android/server/grammaticalinflection/OWNERS
new file mode 100644
index 0000000..5f16ba9
--- /dev/null
+++ b/services/core/java/com/android/server/grammaticalinflection/OWNERS
@@ -0,0 +1,4 @@
+# Bug template url: https://b.corp.google.com/issues/new?component=1082762&template=1601534
+allenwtsu@google.com
+goldmanj@google.com
+calvinpan@google.com
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index c15b538..5b9a663 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -48,12 +48,12 @@
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
+import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY;
 
 import static com.android.server.EventLogTags.IMF_HIDE_IME;
 import static com.android.server.EventLogTags.IMF_SHOW_IME;
 import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT;
 import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_IME_VISIBILITY;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java
index 32479ee..1a2b01e 100644
--- a/services/core/java/com/android/server/pm/AppStateHelper.java
+++ b/services/core/java/com/android/server/pm/AppStateHelper.java
@@ -37,7 +37,7 @@
 import com.android.server.LocalServices;
 
 import java.util.ArrayList;
-import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -159,13 +159,21 @@
         return false;
     }
 
-    private static boolean containsAny(Collection<String> list, Collection<String> which) {
-        if (list.isEmpty()) {
-            return false;
-        }
-        for (var element : which) {
-            if (list.contains(element)) {
+    /**
+     * True if {@code arr} contains any element in {@code which}.
+     * Both {@code arr} and {@code which} must be sorted in advance.
+     */
+    private static boolean containsAny(String[] arr, List<String> which) {
+        int s1 = arr.length;
+        int s2 = which.size();
+        for (int i = 0, j = 0; i < s1 && j < s2; ) {
+            int val = arr[i].compareTo(which.get(j));
+            if (val == 0) {
                 return true;
+            } else if (val < 0) {
+                ++i;
+            } else {
+                ++j;
             }
         }
         return false;
@@ -174,9 +182,9 @@
     private void addLibraryDependency(ArraySet<String> results, List<String> libPackageNames) {
         var pmInternal = LocalServices.getService(PackageManagerInternal.class);
 
-        var libraryNames = new ArraySet<String>();
-        var staticSharedLibraryNames = new ArraySet<String>();
-        var sdkLibraryNames = new ArraySet<String>();
+        var libraryNames = new ArrayList<String>();
+        var staticSharedLibraryNames = new ArrayList<String>();
+        var sdkLibraryNames = new ArrayList<String>();
         for (var packageName : libPackageNames) {
             var pkg = pmInternal.getAndroidPackage(packageName);
             if (pkg == null) {
@@ -199,11 +207,19 @@
             return;
         }
 
-        pmInternal.forEachPackage(pkg -> {
-            if (containsAny(pkg.getUsesLibraries(), libraryNames)
-                    || containsAny(pkg.getUsesOptionalLibraries(), libraryNames)
-                    || containsAny(pkg.getUsesStaticLibraries(), staticSharedLibraryNames)
-                    || containsAny(pkg.getUsesSdkLibraries(), sdkLibraryNames)) {
+        Collections.sort(libraryNames);
+        Collections.sort(sdkLibraryNames);
+        Collections.sort(staticSharedLibraryNames);
+
+        pmInternal.forEachPackageState(pkgState -> {
+            var pkg = pkgState.getPkg();
+            if (pkg == null) {
+                return;
+            }
+            if (containsAny(pkg.getUsesLibrariesSorted(), libraryNames)
+                    || containsAny(pkg.getUsesOptionalLibrariesSorted(), libraryNames)
+                    || containsAny(pkg.getUsesStaticLibrariesSorted(), staticSharedLibraryNames)
+                    || containsAny(pkg.getUsesSdkLibrariesSorted(), sdkLibraryNames)) {
                 results.add(pkg.getPackageName());
             }
         });
diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java
index 1021e07..12f6a18 100644
--- a/services/core/java/com/android/server/pm/AppsFilterBase.java
+++ b/services/core/java/com/android/server/pm/AppsFilterBase.java
@@ -399,6 +399,24 @@
                 Slog.wtf(TAG, "No setting found for non system uid " + callingUid);
                 return true;
             }
+
+            if (DEBUG_TRACING) {
+                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "getAppId");
+            }
+            final int callingAppId = UserHandle.getAppId(callingUid);
+            final int targetAppId = targetPkgSetting.getAppId();
+            if (DEBUG_TRACING) {
+                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+            }
+            if (callingAppId == targetAppId
+                    || callingAppId < Process.FIRST_APPLICATION_UID
+                    || targetAppId < Process.FIRST_APPLICATION_UID) {
+                if (DEBUG_LOGGING) {
+                    log(callingSetting, targetPkgSetting, "same app id or core app id");
+                }
+                return false;
+            }
+
             final PackageStateInternal callingPkgSetting;
             if (DEBUG_TRACING) {
                 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "callingSetting instanceof");
@@ -446,27 +464,6 @@
                 }
             }
 
-            if (DEBUG_TRACING) {
-                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "getAppId");
-            }
-            final int callingAppId;
-            if (callingPkgSetting != null) {
-                callingAppId = callingPkgSetting.getAppId();
-            } else {
-                // all should be the same
-                callingAppId = callingSharedPkgSettings.valueAt(0).getAppId();
-            }
-            final int targetAppId = targetPkgSetting.getAppId();
-            if (DEBUG_TRACING) {
-                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
-            }
-            if (callingAppId == targetAppId) {
-                if (DEBUG_LOGGING) {
-                    log(callingSetting, targetPkgSetting, "same app id");
-                }
-                return false;
-            }
-
             try {
                 if (DEBUG_TRACING) {
                     Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "requestsQueryAllPackages");
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
index cda7503..fe122f8 100644
--- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java
+++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
@@ -614,11 +614,14 @@
             size += getDirectorySize(path);
             if (!ArrayUtils.isEmpty(info.applicationInfo.splitSourceDirs)) {
                 for (String splitSourceDir : info.applicationInfo.splitSourceDirs) {
-                    path = Paths.get(splitSourceDir).toFile();
-                    if (path.isFile()) {
-                        path = path.getParentFile();
+                    File pathSplitSourceDir = Paths.get(splitSourceDir).toFile();
+                    if (pathSplitSourceDir.isFile()) {
+                        pathSplitSourceDir = pathSplitSourceDir.getParentFile();
                     }
-                    size += getDirectorySize(path);
+                    if (path.getAbsolutePath().equals(pathSplitSourceDir.getAbsolutePath())) {
+                        continue;
+                    }
+                    size += getDirectorySize(pathSplitSourceDir);
                 }
             }
             return size;
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageInternal.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageInternal.java
index 8d43fe7..9eca7d6 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageInternal.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageInternal.java
@@ -16,6 +16,8 @@
 
 package com.android.server.pm.parsing.pkg;
 
+import android.annotation.NonNull;
+
 import com.android.internal.content.om.OverlayConfig;
 import com.android.server.pm.pkg.AndroidPackage;
 
@@ -31,5 +33,15 @@
  */
 public interface AndroidPackageInternal extends AndroidPackage,
         OverlayConfig.PackageProvider.Package {
+    @NonNull
+    String[] getUsesLibrariesSorted();
 
+    @NonNull
+    String[] getUsesOptionalLibrariesSorted();
+
+    @NonNull
+    String[] getUsesSdkLibrariesSorted();
+
+    @NonNull
+    String[] getUsesStaticLibrariesSorted();
 }
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
index e361c93..ed9382b 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
@@ -93,6 +93,7 @@
 import java.io.File;
 import java.security.PublicKey;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -405,6 +406,15 @@
     private List<AndroidPackageSplit> mSplits;
 
     @NonNull
+    private String[] mUsesLibrariesSorted;
+    @NonNull
+    private String[] mUsesOptionalLibrariesSorted;
+    @NonNull
+    private String[] mUsesSdkLibrariesSorted;
+    @NonNull
+    private String[] mUsesStaticLibrariesSorted;
+
+    @NonNull
     public static PackageImpl forParsing(@NonNull String packageName, @NonNull String baseCodePath,
             @NonNull String codePath, @NonNull TypedArray manifestArray, boolean isCoreApp) {
         return new PackageImpl(packageName, baseCodePath, codePath, manifestArray, isCoreApp);
@@ -1379,6 +1389,19 @@
 
     @NonNull
     @Override
+    public String[] getUsesLibrariesSorted() {
+        if (mUsesLibrariesSorted == null) {
+            // Note lazy-sorting here doesn't break immutability because it always
+            // return the same content. In the case of multi-threading, data race in accessing
+            // mUsesLibrariesSorted might result in unnecessary creation of sorted copies
+            // which is OK because the case is quite rare.
+            mUsesLibrariesSorted = sortLibraries(usesLibraries);
+        }
+        return mUsesLibrariesSorted;
+    }
+
+    @NonNull
+    @Override
     public List<String> getUsesNativeLibraries() {
         return usesNativeLibraries;
     }
@@ -1391,6 +1414,15 @@
 
     @NonNull
     @Override
+    public String[] getUsesOptionalLibrariesSorted() {
+        if (mUsesOptionalLibrariesSorted == null) {
+            mUsesOptionalLibrariesSorted = sortLibraries(usesOptionalLibraries);
+        }
+        return mUsesOptionalLibrariesSorted;
+    }
+
+    @NonNull
+    @Override
     public List<String> getUsesOptionalNativeLibraries() {
         return usesOptionalNativeLibraries;
     }
@@ -1405,6 +1437,15 @@
     @Override
     public List<String> getUsesSdkLibraries() { return usesSdkLibraries; }
 
+    @NonNull
+    @Override
+    public String[] getUsesSdkLibrariesSorted() {
+        if (mUsesSdkLibrariesSorted == null) {
+            mUsesSdkLibrariesSorted = sortLibraries(usesSdkLibraries);
+        }
+        return mUsesSdkLibrariesSorted;
+    }
+
     @Nullable
     @Override
     public String[][] getUsesSdkLibrariesCertDigests() { return usesSdkLibrariesCertDigests; }
@@ -1419,6 +1460,15 @@
         return usesStaticLibraries;
     }
 
+    @NonNull
+    @Override
+    public String[] getUsesStaticLibrariesSorted() {
+        if (mUsesStaticLibrariesSorted == null) {
+            mUsesStaticLibrariesSorted = sortLibraries(usesStaticLibraries);
+        }
+        return mUsesStaticLibrariesSorted;
+    }
+
     @Nullable
     @Override
     public String[][] getUsesStaticLibrariesCertDigests() {
@@ -2650,6 +2700,16 @@
         return this;
     }
 
+    private static String[] sortLibraries(List<String> libraryNames) {
+        int size = libraryNames.size();
+        if (size == 0) {
+            return EmptyArray.STRING;
+        }
+        var arr = libraryNames.toArray(EmptyArray.STRING);
+        Arrays.sort(arr);
+        return arr;
+    }
+
     private void assignDerivedFields2() {
         mBaseAppInfoFlags = PackageInfoUtils.appInfoFlags(this, null);
         mBaseAppInfoPrivateFlags = PackageInfoUtils.appInfoPrivateFlags(this, null);
diff --git a/services/core/java/com/android/server/power/ShutdownCheckPoints.java b/services/core/java/com/android/server/power/ShutdownCheckPoints.java
index 1a9eae6..32f1bcf 100644
--- a/services/core/java/com/android/server/power/ShutdownCheckPoints.java
+++ b/services/core/java/com/android/server/power/ShutdownCheckPoints.java
@@ -54,6 +54,7 @@
     private static final int MAX_DUMP_FILES = 20;
     private static final SimpleDateFormat DATE_FORMAT =
             new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS z");
+    private static final File[] EMPTY_FILE_ARRAY = {};
 
     private final ArrayList<CheckPoint> mCheckPoints;
     private final Injector mInjector;
@@ -381,6 +382,9 @@
                     return true;
                 }
             });
+            if (files == null) {
+                return EMPTY_FILE_ARRAY;
+            }
             Arrays.sort(files);
             return files;
         }
diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java
index 5ae6973..6c0e1a4 100644
--- a/services/core/java/com/android/server/security/FileIntegrityService.java
+++ b/services/core/java/com/android/server/security/FileIntegrityService.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -59,6 +60,7 @@
  * A {@link SystemService} that provides file integrity related operations.
  * @hide
  */
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
 public class FileIntegrityService extends SystemService {
     private static final String TAG = "FileIntegrityService";
 
@@ -71,7 +73,10 @@
     private final ArrayList<X509Certificate> mTrustedCertificates =
             new ArrayList<X509Certificate>();
 
-    /** Gets the instance of the service */
+    /**
+     * Gets the instance of the service.
+     * @hide
+     */
     public static FileIntegrityService getService() {
         return LocalServices.getService(FileIntegrityService.class);
     }
@@ -139,6 +144,7 @@
         }
     };
 
+    /** @hide */
     public FileIntegrityService(final Context context) {
         super(context);
         try {
@@ -149,6 +155,7 @@
         LocalServices.addService(FileIntegrityService.class, this);
     }
 
+    /** @hide */
     @Override
     public void onStart() {
         loadAllCertificates();
@@ -158,6 +165,7 @@
     /**
      * Returns whether the signature over the file's fs-verity digest can be verified by one of the
      * known certiticates.
+     * @hide
      */
     public boolean verifyPkcs7DetachedSignature(String signaturePath, String filePath)
             throws IOException {
@@ -183,6 +191,16 @@
         return false;
     }
 
+    /**
+     * Enables fs-verity, if supported by the filesystem.
+     * @see <a href="https://www.kernel.org/doc/html/latest/filesystems/fsverity.html">
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+    public static void setUpFsVerity(@NonNull String filePath) throws IOException {
+        VerityUtils.setUpFsverity(filePath);
+    }
+
     private void loadAllCertificates() {
         // A better alternative to load certificates would be to read from .fs-verity kernel
         // keyring, which fsverity_init loads to during earlier boot time from the same sources
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 8613b50..966329e 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -1749,7 +1749,7 @@
                 setExternalControl(true, vibHolder.stats);
             }
             if (DEBUG) {
-                Slog.e(TAG, "Playing external vibration: " + vib);
+                Slog.d(TAG, "Playing external vibration: " + vib);
             }
             // Vibrator will start receiving data from external channels after this point.
             // Report current time as the vibration start time, for debugging.
@@ -1763,7 +1763,7 @@
                 if (mCurrentExternalVibration != null
                         && mCurrentExternalVibration.isHoldingSameVibration(vib)) {
                     if (DEBUG) {
-                        Slog.e(TAG, "Stopping external vibration" + vib);
+                        Slog.d(TAG, "Stopping external vibration: " + vib);
                     }
                     endExternalVibrateLocked(
                             new Vibration.EndInfo(Vibration.Status.FINISHED),
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index fcb135e..11013e8 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -1451,8 +1451,7 @@
                 updatePictureInPictureMode(null, false);
             } else {
                 mLastReportedMultiWindowMode = inMultiWindowMode;
-                ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS,
-                        false /* ignoreVisibility */);
+                ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS);
             }
         }
     }
@@ -3981,6 +3980,7 @@
     }
 
     void finishRelaunching() {
+        mLetterboxUiController.setRelauchingAfterRequestedOrientationChanged(false);
         mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this);
 
         if (mPendingRelaunchCount > 0) {
@@ -7724,13 +7724,17 @@
     }
 
     void setRequestedOrientation(int requestedOrientation) {
+        if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) {
+            return;
+        }
         setOrientation(requestedOrientation, this);
 
         // Push the new configuration to the requested app in case where it's not pushed, e.g. when
         // the request is handled at task level with letterbox.
         if (!getMergedOverrideConfiguration().equals(
                 mLastReportedConfiguration.getMergedConfiguration())) {
-            ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
+            ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */,
+                    false /* ignoreVisibility */, true /* isRequestedOrientationChanged */);
         }
 
         mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged(
@@ -9060,7 +9064,13 @@
 
     boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow) {
         return ensureActivityConfiguration(globalChanges, preserveWindow,
-                false /* ignoreVisibility */);
+                false /* ignoreVisibility */, false /* isRequestedOrientationChanged */);
+    }
+
+    boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow,
+            boolean ignoreVisibility) {
+        return ensureActivityConfiguration(globalChanges, preserveWindow, ignoreVisibility,
+                false /* isRequestedOrientationChanged */);
     }
 
     /**
@@ -9074,11 +9084,13 @@
      *                         (stopped state). This is useful for the case where we know the
      *                         activity will be visible soon and we want to ensure its configuration
      *                         before we make it visible.
+     * @param isRequestedOrientationChanged whether this is triggered in response to an app calling
+     *                                      {@link android.app.Activity#setRequestedOrientation}.
      * @return False if the activity was relaunched and true if it wasn't relaunched because we
      *         can't or the app handles the specific configuration that is changing.
      */
     boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow,
-            boolean ignoreVisibility) {
+            boolean ignoreVisibility, boolean isRequestedOrientationChanged) {
         final Task rootTask = getRootTask();
         if (rootTask.mConfigWillChange) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check "
@@ -9202,6 +9214,9 @@
             } else {
                 mRelaunchReason = RELAUNCH_REASON_NONE;
             }
+            if (isRequestedOrientationChanged) {
+                mLetterboxUiController.setRelauchingAfterRequestedOrientationChanged(true);
+            }
             if (mState == PAUSING) {
                 // A little annoying: we are waiting for this activity to finish pausing. Let's not
                 // do anything now, but just flag that it needs to be restarted when done pausing.
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index b4af69e..034f5c8 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2645,9 +2645,9 @@
 
         if (differentTopTask && !mAvoidMoveToFront) {
             mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
-            if (mSourceRecord == null || (mSourceRootTask.getTopNonFinishingActivity() != null
-                    && mSourceRootTask.getTopNonFinishingActivity().getTask()
-                            == mSourceRecord.getTask())) {
+            // TODO(b/264487981): Consider using BackgroundActivityStartController to determine
+            //  whether to bring the launching activity to the front.
+            if (mSourceRecord == null || inTopNonFinishingTask(mSourceRecord)) {
                 // We really do want to push this one into the user's face, right now.
                 if (mLaunchTaskBehind && mSourceRecord != null) {
                     intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask());
@@ -2706,6 +2706,20 @@
                 mRootWindowContainer.getDefaultTaskDisplayArea(), mTargetRootTask);
     }
 
+    private boolean inTopNonFinishingTask(ActivityRecord r) {
+        if (r == null || r.getTask() == null) {
+            return false;
+        }
+
+        final Task rTask = r.getTask();
+        final Task parent = rTask.getCreatedByOrganizerTask() != null
+                ? rTask.getCreatedByOrganizerTask() : r.getRootTask();
+        final ActivityRecord topNonFinishingActivity = parent != null
+                ? parent.getTopNonFinishingActivity() : null;
+
+        return topNonFinishingActivity != null && topNonFinishingActivity.getTask() == rTask;
+    }
+
     private void resumeTargetRootTaskIfNeeded() {
         if (mDoResume) {
             final ActivityRecord next = mTargetRootTask.topRunningActivity(
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 473a6e5..b39ca5b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1451,6 +1451,7 @@
                 mUserLeaving = true;
             }
 
+            mService.deferWindowLayout();
             final Transition newTransition = task.mTransitionController.isShellTransitionsEnabled()
                     ? task.mTransitionController.isCollecting() ? null
                     : task.mTransitionController.createTransition(TRANSIT_TO_FRONT) : null;
@@ -1458,9 +1459,6 @@
             reason = reason + " findTaskToMoveToFront";
             boolean reparented = false;
             if (task.isResizeable() && canUseActivityOptionsLaunchBounds(options)) {
-                final Rect bounds = options.getLaunchBounds();
-                task.setBounds(bounds);
-
                 Task targetRootTask =
                         mRootWindowContainer.getOrCreateRootTask(null, options, task, ON_TOP);
 
@@ -1473,14 +1471,11 @@
                     // task.reparent() should already placed the task on top,
                     // still need moveTaskToFrontLocked() below for any transition settings.
                 }
-                if (targetRootTask.shouldResizeRootTaskWithLaunchBounds()) {
-                    targetRootTask.resize(bounds, !PRESERVE_WINDOWS, !DEFER_RESUME);
-                } else {
-                    // WM resizeTask must be done after the task is moved to the correct stack,
-                    // because Task's setBounds() also updates dim layer's bounds, but that has
-                    // dependency on the root task.
-                    task.resize(false /* relayout */, false /* forced */);
-                }
+                // The resizeTask must be done after the task is moved to the correct root task,
+                // because Task's setBounds() also updates dim layer's bounds, but that has
+                // dependency on the root task.
+                final Rect bounds = options.getLaunchBounds();
+                task.setBounds(bounds);
             }
 
             if (!reparented) {
@@ -1510,6 +1505,7 @@
             }
         } finally {
             mUserLeaving = false;
+            mService.continueWindowLayout();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index 7bd8c53..6b5f068 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -99,7 +99,7 @@
                     throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
                 }
                 return mService.getRecentTasks().createRecentTaskInfo(task,
-                        false /* stripExtras */, true /* getTasksAllowed */);
+                        false /* stripExtras */);
             } finally {
                 Binder.restoreCallingIdentity(origId);
             }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 82237bb..f2cb755 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -85,6 +85,7 @@
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY;
 import static android.window.DisplayAreaOrganizer.FEATURE_IME;
 import static android.window.DisplayAreaOrganizer.FEATURE_ROOT;
 import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION;
@@ -140,7 +141,6 @@
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowContainerChildProto.DISPLAY_CONTENT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_IME_VISIBILITY;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT_METHOD;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
@@ -500,7 +500,6 @@
     // Accessed directly by all users.
     private boolean mLayoutNeeded;
     int pendingLayoutChanges;
-    boolean mLayoutAndAssignWindowLayersScheduled;
 
     /**
      * Used to gate application window layout until we have sent the complete configuration.
@@ -1109,12 +1108,12 @@
                 * mDisplayMetrics.densityDpi / DENSITY_DEFAULT;
         isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
         mInsetsStateController = new InsetsStateController(this);
+        initializeDisplayBaseInfo();
         mDisplayFrames = new DisplayFrames(mInsetsStateController.getRawInsetsState(),
                 mDisplayInfo, calculateDisplayCutoutForRotation(mDisplayInfo.rotation),
                 calculateRoundedCornersForRotation(mDisplayInfo.rotation),
                 calculatePrivacyIndicatorBoundsForRotation(mDisplayInfo.rotation),
                 calculateDisplayShapeForRotation(mDisplayInfo.rotation));
-        initializeDisplayBaseInfo();
 
         mHoldScreenWakeLock = mWmService.mPowerManager.newWakeLock(
                 PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE,
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 7266d21..ba0413d 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -287,7 +287,7 @@
      *     <li>The activity has fixed orientation but not "locked" or "nosensor" one.
      * </ul>
      */
-    private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
+    boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
         return activity != null && !activity.inMultiWindowMode()
                 && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
                 // "locked" and "nosensor" values are often used by camera apps that can't
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 35e1fbb..1df534f 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -19,7 +19,6 @@
 import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.InsetsController.ANIMATION_TYPE_HIDE;
@@ -42,7 +41,6 @@
 import android.app.WindowConfiguration;
 import android.content.ComponentName;
 import android.content.res.Resources;
-import android.graphics.Rect;
 import android.util.ArrayMap;
 import android.util.IntArray;
 import android.util.SparseArray;
@@ -276,21 +274,22 @@
     /**
      * @see WindowState#getInsetsState()
      */
-    InsetsState getInsetsForWindowMetrics(@NonNull WindowManager.LayoutParams attrs) {
-        final WindowToken token = mDisplayContent.getWindowToken(attrs.token);
-        if (token != null) {
-            final InsetsState rotatedState = token.getFixedRotationTransformInsetsState();
-            if (rotatedState != null) {
-                return rotatedState;
+    void getInsetsForWindowMetrics(@Nullable WindowToken token,
+            @NonNull InsetsState outInsetsState) {
+        final InsetsState srcState = token != null && token.isFixedRotationTransforming()
+                ? token.getFixedRotationTransformInsetsState()
+                : mStateController.getRawInsetsState();
+        outInsetsState.set(srcState, true /* copySources */);
+        for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) {
+            final InsetsSource source = outInsetsState.peekSource(mShowingTransientTypes.get(i));
+            if (source != null) {
+                source.setVisible(false);
             }
         }
-        final boolean alwaysOnTop = token != null && token.isAlwaysOnTop();
-        // Always use windowing mode fullscreen when get insets for window metrics to make sure it
-        // contains all insets types.
-        final InsetsState originalState = enforceInsetsPolicyForTarget(attrs,
-                WINDOWING_MODE_FULLSCREEN, alwaysOnTop, mStateController.getRawInsetsState());
-        InsetsState state = adjustVisibilityForTransientTypes(originalState);
-        return adjustInsetsForRoundedCorners(token, state, state == originalState);
+        adjustInsetsForRoundedCorners(token, outInsetsState, false /* copyState */);
+        if (token != null && token.hasSizeCompatBounds()) {
+            outInsetsState.scale(1f / token.getCompatScale());
+        }
     }
 
     /**
@@ -423,10 +422,9 @@
             final Task task = activityRecord != null ? activityRecord.getTask() : null;
             if (task != null && !task.getWindowConfiguration().tasksAreFloating()) {
                 // Use task bounds to calculating rounded corners if the task is not floating.
-                final Rect roundedCornerFrame = new Rect(task.getBounds());
                 final InsetsState state = copyState ? new InsetsState(originalState)
                         : originalState;
-                state.setRoundedCornerFrame(roundedCornerFrame);
+                state.setRoundedCornerFrame(task.getBounds());
                 return state;
             }
         }
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 5171f5b..659f8d7 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -33,7 +33,6 @@
 import static com.android.server.wm.InsetsSourceProviderProto.SERVER_VISIBLE;
 import static com.android.server.wm.InsetsSourceProviderProto.SOURCE;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_INSETS_CONTROL;
-import static com.android.server.wm.WindowManagerService.H.LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -508,11 +507,6 @@
             return;
         }
         mClientVisible = clientVisible;
-        if (!mDisplayContent.mLayoutAndAssignWindowLayersScheduled) {
-            mDisplayContent.mLayoutAndAssignWindowLayersScheduled = true;
-            mDisplayContent.mWmService.mH.obtainMessage(
-                    LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED, mDisplayContent).sendToTarget();
-        }
         updateVisibility();
     }
 
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 9b84233..5e4f2ae 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -222,6 +222,11 @@
     // See RefreshCallbackItem for context.
     private boolean mIsCameraCompatRefreshCycleThroughStopEnabled = true;
 
+    // Whether should ignore app requested orientation in response to an app
+    // calling Activity#setRequestedOrientation. See
+    // LetterboxUiController#shouldIgnoreRequestedOrientation for details.
+    private final boolean mIsPolicyForIgnoringRequestedOrientationEnabled;
+
     LetterboxConfiguration(Context systemUiContext) {
         this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
                 () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
@@ -274,10 +279,13 @@
                 R.bool.config_letterboxIsEnabledForTranslucentActivities);
         mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
                 R.bool.config_isWindowManagerCameraCompatTreatmentEnabled);
+        mIsCompatFakeFocusEnabled = mContext.getResources().getBoolean(
+                R.bool.config_isCompatFakeFocusEnabled);
+        mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean(
+                R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled);
+
         mLetterboxConfigurationPersister = letterboxConfigurationPersister;
         mLetterboxConfigurationPersister.start();
-        mIsCompatFakeFocusEnabled = mContext.getResources()
-                .getBoolean(R.bool.config_isCompatFakeFocusEnabled);
     }
 
     /**
@@ -1034,6 +1042,15 @@
         mIsCompatFakeFocusEnabled = enabled;
     }
 
+    /**
+     * Whether should ignore app requested orientation in response to an app calling
+     * {@link android.app.Activity#setRequestedOrientation}. See {@link
+     * LetterboxUiController#shouldIgnoreRequestedOrientation} for details.
+     */
+    boolean isPolicyForIgnoringRequestedOrientationEnabled() {
+        return mIsPolicyForIgnoringRequestedOrientationEnabled;
+    }
+
     /** Whether camera compatibility treatment is enabled. */
     boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
         return mIsCameraCompatTreatmentEnabled
@@ -1044,7 +1061,7 @@
     // DeviceConfig.OnPropertiesChangedListener
     private static boolean isCameraCompatTreatmentAllowed() {
         return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                "enable_camera_compat_treatment", false);
+                "enable_compat_camera_treatment", true);
     }
 
     /** Whether camera compatibility refresh is enabled. */
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index fd7e082..0c8a645 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -17,10 +17,13 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.screenOrientationToString;
 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;
+import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
@@ -55,6 +58,8 @@
 
 import android.annotation.Nullable;
 import android.app.ActivityManager.TaskDescription;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Color;
@@ -74,6 +79,7 @@
 import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
 
 import java.io.PrintWriter;
+import java.util.function.BooleanSupplier;
 
 /** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */
 // TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in
@@ -131,12 +137,37 @@
     // DisplayRotationCompatPolicy.
     private boolean mIsRefreshAfterRotationRequested;
 
+    @Nullable
+    private final Boolean mBooleanPropertyIgnoreRequestedOrientation;
+
+    private boolean mIsRelauchingAfterRequestedOrientationChanged;
+
     LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
         mLetterboxConfiguration = wmService.mLetterboxConfiguration;
         // Given activityRecord may not be fully constructed since LetterboxUiController
         // is created in its constructor. It shouldn't be used in this constructor but it's safe
         // to use it after since controller is only used in ActivityRecord.
         mActivityRecord = activityRecord;
+
+        PackageManager packageManager = wmService.mContext.getPackageManager();
+        mBooleanPropertyIgnoreRequestedOrientation =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
+                        PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+    }
+
+    @Nullable
+    private static Boolean readComponentProperty(PackageManager packageManager, String packageName,
+            BooleanSupplier gatingCondition, String propertyName) {
+        if (!gatingCondition.getAsBoolean()) {
+            return null;
+        }
+        try {
+            return packageManager.getProperty(propertyName, packageName).getBoolean();
+        } catch (PackageManager.NameNotFoundException e) {
+            // No such property name.
+        }
+        return null;
     }
 
     /** Cleans up {@link Letterbox} if it exists.*/
@@ -154,6 +185,72 @@
     }
 
     /**
+     * Whether should ignore app requested orientation in response to an app
+     * calling {@link android.app.Activity#setRequestedOrientation}.
+     *
+     * <p>This is needed to avoid getting into {@link android.app.Activity#setRequestedOrientation}
+     * loop when {@link DisplayContent#getIgnoreOrientationRequest} is enabled or device has
+     * landscape natural orientation which app developers don't expect. For example, the loop can
+     * look like this:
+     * <ol>
+     *     <li>App sets default orientation to "unspecified" at runtime
+     *     <li>App requests to "portrait" after checking some condition (e.g. display rotation).
+     *     <li>(2) leads to fullscreen -> letterboxed bounds change and activity relaunch because
+     *     app can't handle the corresponding config changes.
+     *     <li>Loop goes back to (1)
+     * </ol>
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the treatment is enabled
+     *     <li>Opt-out component property isn't enabled
+     *     <li>Opt-in component property or per-app override are enabled
+     *     <li>Activity is relaunched after {@link android.app.Activity#setRequestedOrientation}
+     *     call from an app or camera compat force rotation treatment is active for the activity.
+     * </ul>
+     */
+    boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) {
+        if (!mLetterboxConfiguration.isPolicyForIgnoringRequestedOrientationEnabled()) {
+            return false;
+        }
+        if (Boolean.FALSE.equals(mBooleanPropertyIgnoreRequestedOrientation)) {
+            return false;
+        }
+        if (!Boolean.TRUE.equals(mBooleanPropertyIgnoreRequestedOrientation)
+                && !mActivityRecord.info.isChangeEnabled(
+                        OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION)) {
+            return false;
+        }
+        if (mIsRelauchingAfterRequestedOrientationChanged) {
+            Slog.w(TAG, "Ignoring orientation update to "
+                    + screenOrientationToString(requestedOrientation)
+                    + " due to relaunching after setRequestedOrientation for " + mActivityRecord);
+            return true;
+        }
+        DisplayContent displayContent = mActivityRecord.mDisplayContent;
+        if (displayContent == null) {
+            return false;
+        }
+        if (displayContent.mDisplayRotationCompatPolicy != null
+                && displayContent.mDisplayRotationCompatPolicy
+                        .isTreatmentEnabledForActivity(mActivityRecord)) {
+            Slog.w(TAG, "Ignoring orientation update to "
+                    + screenOrientationToString(requestedOrientation)
+                    + " due to camera compat treatment for " + mActivityRecord);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Sets whether an activity is relaunching after the app has called {@link
+     * android.app.Activity#setRequestedOrientation}.
+     */
+    void setRelauchingAfterRequestedOrientationChanged(boolean isRelaunching) {
+        mIsRelauchingAfterRequestedOrientationChanged = isRelaunching;
+    }
+
+    /**
      * Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked}
      * following the camera compat force rotation in {@link DisplayRotationCompatPolicy}.
      */
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 4be1c83..9e95918 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -976,7 +976,7 @@
                 continue;
             }
 
-            res.add(createRecentTaskInfo(task, true /* stripExtras */, getTasksAllowed));
+            res.add(createRecentTaskInfo(task, true /* stripExtras */));
         }
         return res;
     }
@@ -1889,8 +1889,7 @@
     /**
      * Creates a new RecentTaskInfo from a Task.
      */
-    ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras,
-            boolean getTasksAllowed) {
+    ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) {
         final ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo();
         // If the recent Task is detached, we consider it will be re-attached to the default
         // TaskDisplayArea because we currently only support recent overview in the default TDA.
@@ -1902,9 +1901,6 @@
         rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID;
         rti.persistentId = rti.taskId;
         rti.lastSnapshotData.set(tr.mLastTaskSnapshotData);
-        if (!getTasksAllowed) {
-            Task.trimIneffectiveInfo(tr, rti);
-        }
 
         // Fill in organized child task info for the task created by organizer.
         if (tr.mCreatedByOrganizer) {
diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java
index 1cc1a57..614b405 100644
--- a/services/core/java/com/android/server/wm/RunningTasks.java
+++ b/services/core/java/com/android/server/wm/RunningTasks.java
@@ -173,10 +173,6 @@
         }
         // Fill in some deprecated values
         rti.id = rti.taskId;
-
-        if (!mAllowed) {
-            Task.trimIneffectiveInfo(task, rti);
-        }
         return rti;
     }
 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 8609e10..3b5b5a9 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -423,9 +423,6 @@
     // This number will be assigned when we evaluate OOM scores for all visible tasks.
     int mLayerRank = LAYER_RANK_INVISIBLE;
 
-    /** Helper object used for updating override configuration. */
-    private Configuration mTmpConfig = new Configuration();
-
     /* Unique identifier for this task. */
     final int mTaskId;
     /* User for which this task was created. */
@@ -796,16 +793,11 @@
 
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "resizeTask_" + mTaskId);
 
-            boolean updatedConfig = false;
-            mTmpConfig.setTo(getResolvedOverrideConfiguration());
-            if (setBounds(bounds) != BOUNDS_CHANGE_NONE) {
-                updatedConfig = !mTmpConfig.equals(getResolvedOverrideConfiguration());
-            }
             // This variable holds information whether the configuration didn't change in a
             // significant way and the activity was kept the way it was. If it's false, it means
             // the activity had to be relaunched due to configuration change.
             boolean kept = true;
-            if (updatedConfig) {
+            if (setBounds(bounds, forced) != BOUNDS_CHANGE_NONE) {
                 final ActivityRecord r = topRunningActivityLocked();
                 if (r != null) {
                     kept = r.ensureActivityConfiguration(0 /* globalChanges */,
@@ -822,8 +814,6 @@
                     }
                 }
             }
-            resize(kept, forced);
-
             saveLaunchingStateIfNeeded();
 
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -2693,12 +2683,6 @@
         return canSpecifyOrientation() && getDisplayArea().canSpecifyOrientation(orientation);
     }
 
-    void resize(boolean relayout, boolean forced) {
-        if (setBounds(getRequestedOverrideBounds(), forced) != BOUNDS_CHANGE_NONE && relayout) {
-            getDisplayContent().layoutAndAssignWindowLayersIfNeeded();
-        }
-    }
-
     @Override
     void onDisplayChanged(DisplayContent dc) {
         final boolean isRootTask = isRootTask();
@@ -3418,27 +3402,6 @@
         info.isSleeping = shouldSleepActivities();
     }
 
-    /**
-     * Removes the activity info if the activity belongs to a different uid, which is
-     * different from the app that hosts the task.
-     */
-    static void trimIneffectiveInfo(Task task, TaskInfo info) {
-        final ActivityRecord baseActivity = task.getActivity(r -> !r.finishing,
-                false /* traverseTopToBottom */);
-        final int baseActivityUid =
-                baseActivity != null ? baseActivity.getUid() : task.effectiveUid;
-
-        if (info.topActivityInfo != null
-                && task.effectiveUid != info.topActivityInfo.applicationInfo.uid) {
-            info.topActivity = null;
-            info.topActivityInfo = null;
-        }
-
-        if (task.effectiveUid != baseActivityUid) {
-            info.baseActivity = null;
-        }
-    }
-
     @Nullable PictureInPictureParams getPictureInPictureParams() {
         final Task topTask = getTopMostTask();
         if (topTask == null) return null;
@@ -4771,14 +4734,6 @@
         }
     }
 
-    /**
-     * Returns true if this root task should be resized to match the bounds specified by
-     * {@link ActivityOptions#setLaunchBounds} when launching an activity into the root task.
-     */
-    boolean shouldResizeRootTaskWithLaunchBounds() {
-        return inPinnedWindowingMode();
-    }
-
     void checkTranslucentActivityWaiting(ActivityRecord top) {
         if (mTranslucentActivityWaiting != top) {
             mUndrawnActivitiesBelowTopTranslucent.clear();
@@ -5603,29 +5558,6 @@
         return true;
     }
 
-    // TODO: Can only be called from special methods in ActivityTaskSupervisor.
-    // Need to consolidate those calls points into this resize method so anyone can call directly.
-    void resize(Rect displayedBounds, boolean preserveWindows, boolean deferResume) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "task.resize_" + getRootTaskId());
-        mAtmService.deferWindowLayout();
-        try {
-            // TODO: Why not just set this on the root task directly vs. on each tasks?
-            // Update override configurations of all tasks in the root task.
-            forAllTasks(task -> {
-                if (task.isResizeable()) {
-                    task.setBounds(displayedBounds);
-                }
-            }, true /* traverseTopToBottom */);
-
-            if (!deferResume) {
-                ensureVisibleActivitiesConfiguration(topRunningActivity(), preserveWindows);
-            }
-        } finally {
-            mAtmService.continueWindowLayout();
-            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-        }
-    }
-
     boolean willActivityBeVisible(IBinder token) {
         final ActivityRecord r = ActivityRecord.forTokenLocked(token);
         if (r == null) {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index b887861..dd489aa 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -96,6 +96,7 @@
 import android.view.SurfaceControl;
 import android.window.ITaskFragmentOrganizer;
 import android.window.ScreenCapture;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentInfo;
 import android.window.TaskFragmentOrganizerToken;
 
@@ -306,6 +307,10 @@
     @Nullable
     private final IBinder mFragmentToken;
 
+    /** The animation override params for animation running on this TaskFragment. */
+    @NonNull
+    private TaskFragmentAnimationParams mAnimationParams = TaskFragmentAnimationParams.DEFAULT;
+
     /**
      * The bounds of the embedded TaskFragment relative to the parent Task.
      * {@code null} if it is not {@link #mIsEmbedded}
@@ -453,6 +458,15 @@
                 && organizer.asBinder().equals(mTaskFragmentOrganizer.asBinder());
     }
 
+    void setAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) {
+        mAnimationParams = animationParams;
+    }
+
+    @NonNull
+    TaskFragmentAnimationParams getAnimationParams() {
+        return mAnimationParams;
+    }
+
     TaskFragment getAdjacentTaskFragment() {
         return mAdjacentTaskFragment;
     }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 302538f..ad1e879 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -647,6 +647,7 @@
                 t.setCornerRadius(targetLeash, 0);
                 t.setShadowRadius(targetLeash, 0);
                 t.setMatrix(targetLeash, 1, 0, 0, 1);
+                t.setAlpha(targetLeash, 1);
                 // The bounds sent to the transition is always a real bounds. This means we lose
                 // information about "null" bounds (inheriting from parent). Core will fix-up
                 // non-organized window surface bounds; however, since Core can't touch organized
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 5de143d9..7f9e808 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -756,9 +756,7 @@
     private void updateWallpaperTokens(boolean visible) {
         for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) {
             final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx);
-            if (token.updateWallpaperWindows(visible)) {
-                token.mDisplayContent.assignWindowLayers(false /* setLayoutNeeded */);
-            }
+            token.updateWallpaperWindows(visible);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 87f4ad4..210d5a5 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -100,7 +100,7 @@
     }
 
     /** Returns {@code true} if visibility is changed. */
-    boolean updateWallpaperWindows(boolean visible) {
+    void updateWallpaperWindows(boolean visible) {
         boolean changed = false;
         if (mVisibleRequested != visible) {
             ProtoLog.d(WM_DEBUG_WALLPAPER, "Wallpaper token %s visible=%b",
@@ -117,7 +117,6 @@
                     linkFixedRotationTransform(wallpaperTarget.mToken);
                 }
             }
-            return changed;
         }
 
         final WindowState wallpaperTarget =
@@ -143,7 +142,6 @@
         }
 
         setVisible(visible);
-        return changed;
     }
 
     private void setVisible(boolean visible) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 6737052..9e94fdf 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -265,14 +265,6 @@
     }
 
     /**
-     * Callback which is triggered while changing the parent, after setting up the surface but
-     * before asking the parent to assign child layers.
-     */
-    interface PreAssignChildLayersCallback {
-        void onPreAssignChildLayers();
-    }
-
-    /**
      * True if this an AppWindowToken and the activity which created this was launched with
      * ActivityOptions.setLaunchTaskBehind.
      *
@@ -605,11 +597,6 @@
      */
     @Override
     void onParentChanged(ConfigurationContainer newParent, ConfigurationContainer oldParent) {
-        onParentChanged(newParent, oldParent, null);
-    }
-
-    void onParentChanged(ConfigurationContainer newParent, ConfigurationContainer oldParent,
-            PreAssignChildLayersCallback callback) {
         super.onParentChanged(newParent, oldParent);
         if (mParent == null) {
             return;
@@ -627,13 +614,8 @@
             reparentSurfaceControl(getSyncTransaction(), mParent.mSurfaceControl);
         }
 
-        if (callback != null) {
-            callback.onPreAssignChildLayers();
-        }
-
         // Either way we need to ask the parent to assign us a Z-order.
         mParent.assignChildLayers();
-        scheduleAnimation();
     }
 
     void createSurfaceControl(boolean force) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
index 6abb8fd..42b556f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
+++ b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
@@ -56,7 +56,4 @@
     static final boolean SHOW_STACK_CRAWLS = false;
     static final boolean DEBUG_WINDOW_CROP = false;
     static final boolean DEBUG_UNKNOWN_APP_VISIBILITY = false;
-
-    // TODO(b/239501597) : Have a system property to control this flag.
-    public static final boolean DEBUG_IME_VISIBILITY = false;
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b83f423..2c8b844 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -5358,7 +5358,6 @@
         public static final int ANIMATION_FAILSAFE = 60;
         public static final int RECOMPUTE_FOCUS = 61;
         public static final int ON_POINTER_DOWN_OUTSIDE_FOCUS = 62;
-        public static final int LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED = 63;
         public static final int WINDOW_STATE_BLAST_SYNC_TIMEOUT = 64;
         public static final int REPARENT_TASK_TO_DEFAULT_DISPLAY = 65;
         public static final int INSETS_CHANGED = 66;
@@ -5635,14 +5634,6 @@
                     }
                     break;
                 }
-                case LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED: {
-                    synchronized (mGlobalLock) {
-                        final DisplayContent displayContent = (DisplayContent) msg.obj;
-                        displayContent.mLayoutAndAssignWindowLayersScheduled = false;
-                        displayContent.layoutAndAssignWindowLayersIfNeeded();
-                    }
-                    break;
-                }
                 case WINDOW_STATE_BLAST_SYNC_TIMEOUT: {
                     synchronized (mGlobalLock) {
                         final WindowState ws = (WindowState) msg.obj;
@@ -8924,28 +8915,17 @@
     }
 
     @Override
-    public boolean getWindowInsets(WindowManager.LayoutParams attrs, int displayId,
-            InsetsState outInsetsState) {
-        final int uid = Binder.getCallingUid();
+    public boolean getWindowInsets(int displayId, IBinder token, InsetsState outInsetsState) {
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized (mGlobalLock) {
-                final DisplayContent dc = getDisplayContentOrCreate(displayId, attrs.token);
+                final DisplayContent dc = getDisplayContentOrCreate(displayId, token);
                 if (dc == null) {
                     throw new WindowManager.InvalidDisplayException("Display#" + displayId
                             + "could not be found!");
                 }
-                final WindowToken token = dc.getWindowToken(attrs.token);
-                final float overrideScale = mAtmService.mCompatModePackages.getCompatScale(
-                        attrs.packageName, uid);
-                final InsetsState state = dc.getInsetsPolicy().getInsetsForWindowMetrics(attrs);
-                outInsetsState.set(state, true /* copySources */);
-                if (WindowState.hasCompatScale(attrs, token, overrideScale)) {
-                    final float compatScale = token != null && token.hasSizeCompatBounds()
-                            ? token.getCompatScale() * overrideScale
-                            : overrideScale;
-                    outInsetsState.scale(1f / compatScale);
-                }
+                final WindowToken winToken = dc.getWindowToken(token);
+                dc.getInsetsPolicy().getInsetsForWindowMetrics(winToken, outInsetsState);
                 return dc.getDisplayPolicy().areSystemBarsForcedConsumedLw();
             }
         } finally {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index b624e80..7d15902 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -21,6 +21,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS;
@@ -44,6 +45,7 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_SHORTCUT;
 
@@ -88,7 +90,9 @@
 import android.window.ITransitionPlayer;
 import android.window.IWindowContainerTransactionCallback;
 import android.window.IWindowOrganizerController;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentCreationParams;
+import android.window.TaskFragmentOperation;
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -658,7 +662,8 @@
             }
         }
 
-        if (windowingMode > -1) {
+        final int prevWindowingMode = container.getWindowingMode();
+        if (windowingMode > -1 && prevWindowingMode != windowingMode) {
             if (mService.isInLockTaskMode()
                     && WindowConfiguration.inMultiWindowMode(windowingMode)) {
                 throw new UnsupportedOperationException("Not supported to set multi-window"
@@ -672,9 +677,8 @@
                 return effects;
             }
 
-            final int prevMode = container.getWindowingMode();
             container.setWindowingMode(windowingMode);
-            if (prevMode != container.getWindowingMode()) {
+            if (prevWindowingMode != container.getWindowingMode()) {
                 // The activity in the container may become focusable or non-focusable due to
                 // windowing modes changes (such as entering or leaving pinned windowing mode),
                 // so also apply the lifecycle effects to this transaction.
@@ -1138,6 +1142,10 @@
                 fragment.setCompanionTaskFragment(companion);
                 break;
             }
+            case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION: {
+                effects |= applyTaskFragmentOperation(hop, errorCallbackToken, organizer);
+                break;
+            }
             default: {
                 // The other operations may change task order so they are skipped while in lock
                 // task mode. The above operations are still allowed because they don't move
@@ -1270,6 +1278,47 @@
         return effects;
     }
 
+    /** Applies change set through {@link WindowContainerTransaction#setTaskFragmentOperation}. */
+    private int applyTaskFragmentOperation(@NonNull WindowContainerTransaction.HierarchyOp hop,
+            @Nullable IBinder errorCallbackToken, @Nullable ITaskFragmentOrganizer organizer) {
+        final IBinder fragmentToken = hop.getContainer();
+        final TaskFragment taskFragment = mLaunchTaskFragments.get(fragmentToken);
+        final TaskFragmentOperation operation = hop.getTaskFragmentOperation();
+        if (operation == null) {
+            final Throwable exception = new IllegalArgumentException(
+                    "TaskFragmentOperation must be non-null");
+            sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment,
+                    HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception);
+            return 0;
+        }
+        final int opType = operation.getOpType();
+        if (taskFragment == null || !taskFragment.isAttached()) {
+            final Throwable exception = new IllegalArgumentException(
+                    "Not allowed to apply operation on invalid fragment tokens opType=" + opType);
+            sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment,
+                    HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception);
+            return 0;
+        }
+
+        int effect = 0;
+        switch (opType) {
+            case OP_TYPE_SET_ANIMATION_PARAMS: {
+                final TaskFragmentAnimationParams animationParams = operation.getAnimationParams();
+                if (animationParams == null) {
+                    final Throwable exception = new IllegalArgumentException(
+                            "TaskFragmentAnimationParams must be non-null");
+                    sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment,
+                            HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception);
+                    break;
+                }
+                taskFragment.setAnimationParams(animationParams);
+                break;
+            }
+            // TODO(b/263436063): move other TaskFragment related operation here.
+        }
+        return effect;
+    }
+
     /** A helper method to send minimum dimension violation error to the client. */
     private void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions,
             IBinder errorCallbackToken, String reason) {
@@ -1698,6 +1747,7 @@
                     break;
                 case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
                 case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT:
+                case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION:
                     enforceTaskFragmentOrganized(func, hop.getContainer(), organizer);
                     break;
                 case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT:
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index ae31ee8..59c6737 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -364,7 +364,7 @@
     boolean mHidden = true;    // Used to determine if to show child windows.
     private boolean mDragResizing;
     private boolean mDragResizingChangeReported = true;
-    private boolean mRedrawForSyncReported;
+    private boolean mRedrawForSyncReported = true;
 
     /**
      * Used to assosciate a given set of state changes sent from MSG_RESIZED
@@ -1276,24 +1276,15 @@
      * @see ActivityRecord#hasSizeCompatBounds()
      */
     boolean hasCompatScale() {
-        return hasCompatScale(mAttrs, mActivityRecord, mOverrideScale);
-    }
-
-    /**
-     * @return {@code true} if the application runs in size compatibility mode.
-     * @see android.content.res.CompatibilityInfo#supportsScreen
-     * @see ActivityRecord#hasSizeCompatBounds()
-     */
-    static boolean hasCompatScale(WindowManager.LayoutParams attrs, WindowToken token,
-            float overrideScale) {
-        if ((attrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) {
+        if ((mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) {
             return true;
         }
-        if (attrs.type == TYPE_APPLICATION_STARTING) {
+        if (mAttrs.type == TYPE_APPLICATION_STARTING) {
             // Exclude starting window because it is not displayed by the application.
             return false;
         }
-        return token != null && token.hasSizeCompatBounds() || overrideScale != 1f;
+        return mActivityRecord != null && mActivityRecord.hasSizeCompatBounds()
+                || mOverrideScale != 1f;
     }
 
     /**
@@ -5956,8 +5947,11 @@
         mSyncSeqId++;
         if (getSyncMethod() == BLASTSyncEngine.METHOD_BLAST) {
             mPrepareSyncSeqId = mSyncSeqId;
+            requestRedrawForSync();
+        } else if (mHasSurface && mWinAnimator.mDrawState != DRAW_PENDING) {
+            // Only need to request redraw if the window has reported draw.
+            requestRedrawForSync();
         }
-        requestRedrawForSync();
         return true;
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index a0ba8fd..5ecf737 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -428,10 +428,6 @@
         mShownAlpha = mAlpha;
     }
 
-    private boolean isInBlastSync() {
-        return mService.useBLASTSync() && mWin.useBLASTSync();
-    }
-
     void prepareSurfaceLocked(SurfaceControl.Transaction t) {
         final WindowState w = mWin;
         if (!hasSurface()) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 8c2065e..7e93522 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -18,7 +18,13 @@
 
 import static android.Manifest.permission.BIND_DEVICE_ADMIN;
 import static android.Manifest.permission.MANAGE_CA_CERTIFICATES;
+import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS;
+import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL;
+import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL;
+import static android.Manifest.permission.QUERY_ADMIN_POLICY;
 import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY;
+import static android.Manifest.permission.SET_TIME;
+import static android.Manifest.permission.SET_TIME_ZONE;
 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
@@ -137,6 +143,7 @@
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
@@ -715,7 +722,10 @@
                     + "management app's authentication policy";
     private static final String NOT_SYSTEM_CALLER_MSG = "Only the system can %s";
 
+    private static final String PERMISSION_BASED_ACCESS_EXPERIMENT_FLAG =
+            "enable_permission_based_access";
     private static final String ENABLE_COEXISTENCE_FLAG = "enable_coexistence";
+    private static final boolean DEFAULT_VALUE_PERMISSION_BASED_ACCESS_FLAG = false;
     private static final boolean DEFAULT_ENABLE_COEXISTENCE_FLAG = false;
 
     // TODO(b/258425381) remove the flag after rollout.
@@ -8033,9 +8043,15 @@
         Objects.requireNonNull(who, "ComponentName is null");
 
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
-                || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller));
 
+        if (isPermissionCheckFlagEnabled()) {
+            // The effect of this policy is device-wide.
+            enforcePermission(SET_TIME, UserHandle.USER_ALL);
+        } else {
+            Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
+                    || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(
+                    caller));
+        }
         mInjector.binderWithCleanCallingIdentity(() ->
                 mInjector.settingsGlobalPutInt(Settings.Global.AUTO_TIME, enabled ? 1 : 0));
 
@@ -8057,8 +8073,14 @@
         Objects.requireNonNull(who, "ComponentName is null");
 
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
-                || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller));
+
+        if (isPermissionCheckFlagEnabled()) {
+            enforceCanQuery(SET_TIME, UserHandle.USER_ALL);
+        } else {
+            Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
+                    || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(
+                    caller));
+        }
 
         return mInjector.settingsGlobalGetInt(Global.AUTO_TIME, 0) > 0;
     }
@@ -8074,8 +8096,15 @@
         Objects.requireNonNull(who, "ComponentName is null");
 
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
-                || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller));
+
+        if (isPermissionCheckFlagEnabled()) {
+            // The effect of this policy is device-wide.
+            enforcePermission(SET_TIME_ZONE, UserHandle.USER_ALL);
+        } else {
+            Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
+                    || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(
+                    caller));
+        }
 
         if (isCoexistenceEnabled(caller)) {
             mDevicePolicyEngine.setGlobalPolicy(
@@ -8107,8 +8136,15 @@
         Objects.requireNonNull(who, "ComponentName is null");
 
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
-                || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller));
+
+        if (isPermissionCheckFlagEnabled()) {
+            // The effect of this policy is device-wide.
+            enforceCanQuery(SET_TIME_ZONE, UserHandle.USER_ALL);
+        } else {
+            Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
+                    || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(
+                    caller));
+        }
 
         return mInjector.settingsGlobalGetInt(Global.AUTO_TIME_ZONE, 0) > 0;
     }
@@ -13031,8 +13067,14 @@
         Objects.requireNonNull(who, "ComponentName is null");
 
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(
-                isDefaultDeviceOwner(caller) || isProfileOwnerOfOrganizationOwnedDevice(caller));
+        if (isPermissionCheckFlagEnabled()) {
+            // This is a global action.
+            enforcePermission(SET_TIME, UserHandle.USER_ALL);
+        } else {
+            Preconditions.checkCallAuthorization(
+                    isDefaultDeviceOwner(caller)
+                            || isProfileOwnerOfOrganizationOwnedDevice(caller));
+        }
 
         // Don't allow set time when auto time is on.
         if (mInjector.settingsGlobalGetInt(Global.AUTO_TIME, 0) == 1) {
@@ -13051,8 +13093,14 @@
         Objects.requireNonNull(who, "ComponentName is null");
 
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(
-                isDefaultDeviceOwner(caller) || isProfileOwnerOfOrganizationOwnedDevice(caller));
+        if (isPermissionCheckFlagEnabled()) {
+            // This is a global action.
+            enforcePermission(SET_TIME_ZONE, UserHandle.USER_ALL);
+        } else {
+            Preconditions.checkCallAuthorization(
+                    isDefaultDeviceOwner(caller)
+                            || isProfileOwnerOfOrganizationOwnedDevice(caller));
+        }
 
         // Don't allow set timezone when auto timezone is on.
         if (mInjector.settingsGlobalGetInt(Global.AUTO_TIME_ZONE, 0) == 1) {
@@ -13657,6 +13705,16 @@
             broadcastIntentToDevicePolicyManagerRoleHolder(intent, parentHandle);
         }
 
+        @Override
+        public void enforcePermission(String permission, int targetUserId) {
+            DevicePolicyManagerService.this.enforcePermission(permission, targetUserId);
+        }
+
+        @Override
+        public boolean hasPermission(String permission, int targetUserId) {
+            return DevicePolicyManagerService.this.hasPermission(permission, targetUserId);
+        }
+
         private void broadcastIntentToCrossProfileManifestReceivers(
                 Intent intent, UserHandle userHandle, boolean requiresPermission) {
             final int userId = userHandle.getIdentifier();
@@ -18408,14 +18466,25 @@
         }
     }
 
+
     private String getDevicePolicyManagementRoleHolderPackageName(Context context) {
         RoleManager roleManager = context.getSystemService(RoleManager.class);
-        List<String> roleHolders =
-                roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT);
-        if (roleHolders.isEmpty()) {
-            return null;
-        }
-        return roleHolders.get(0);
+
+        // Calling identity needs to be cleared as this method is used in the permissions checks.
+        return mInjector.binderWithCleanCallingIdentity(() -> {
+            List<String> roleHolders =
+                    roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT);
+            if (roleHolders.isEmpty()) {
+                return null;
+            }
+            return roleHolders.get(0);
+        });
+    }
+
+    private boolean isDevicePolicyManagementRoleHolder(CallerIdentity caller) {
+        String devicePolicyManagementRoleHolderPackageName =
+                getDevicePolicyManagementRoleHolderPackageName(mContext);
+        return caller.getPackageName().equals(devicePolicyManagementRoleHolderPackageName);
     }
 
     private void resetInteractAcrossProfilesAppOps() {
@@ -19513,6 +19582,192 @@
         });
     }
 
+    // DPC types
+    private static final int DEFAULT_DEVICE_OWNER = 0;
+    private static final int FINANCED_DEVICE_OWNER = 1;
+    private static final int PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE = 2;
+    private static final int PROFILE_OWNER_ON_USER_0 = 3;
+    private static final int PROFILE_OWNER = 4;
+
+    // Permissions of existing DPC types.
+    private static final List<String> DEFAULT_DEVICE_OWNER_PERMISSIONS = List.of(
+            MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL,
+            MANAGE_DEVICE_POLICY_ACROSS_USERS,
+            MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL,
+            SET_TIME,
+            SET_TIME_ZONE);
+    private static final List<String> FINANCED_DEVICE_OWNER_PERMISSIONS = List.of(
+            MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL,
+            MANAGE_DEVICE_POLICY_ACROSS_USERS,
+            MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL);
+    private static final List<String> PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE_PERMISSIONS =
+            List.of(
+                MANAGE_DEVICE_POLICY_ACROSS_USERS,
+                MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL,
+                SET_TIME,
+                SET_TIME_ZONE);
+    private static final List<String> PROFILE_OWNER_ON_USER_0_PERMISSIONS  = List.of(
+            SET_TIME,
+            SET_TIME_ZONE);
+    private static final List<String> PROFILE_OWNER_PERMISSIONS  = List.of(
+            MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL);
+
+    private static final HashMap<Integer, List<String>> DPC_PERMISSIONS = new HashMap<>();
+    {
+        DPC_PERMISSIONS.put(DEFAULT_DEVICE_OWNER, DEFAULT_DEVICE_OWNER_PERMISSIONS);
+        DPC_PERMISSIONS.put(FINANCED_DEVICE_OWNER, FINANCED_DEVICE_OWNER_PERMISSIONS);
+        DPC_PERMISSIONS.put(PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE,
+                PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE_PERMISSIONS);
+        DPC_PERMISSIONS.put(PROFILE_OWNER_ON_USER_0, PROFILE_OWNER_ON_USER_0_PERMISSIONS);
+        DPC_PERMISSIONS.put(PROFILE_OWNER, PROFILE_OWNER_PERMISSIONS);
+    }
+
+    //TODO(b/254253251) Fill this map in as new permissions are added for policies.
+    private static final HashMap<String, Integer> ACTIVE_ADMIN_POLICIES = new HashMap<>();
+
+    private static final HashMap<String, String> CROSS_USER_PERMISSIONS =
+            new HashMap<>();
+    {
+        // Auto time is intrinsically global so there is no cross-user permission.
+        CROSS_USER_PERMISSIONS.put(SET_TIME, null);
+        CROSS_USER_PERMISSIONS.put(SET_TIME_ZONE, null);
+    }
+
+    /**
+     * Checks if the calling process has been granted permission to apply a device policy on a
+     * specific user.
+     * The given permission will be checked along with its associated cross-user permission if it
+     * exists and the target user is different to the calling user.
+     *
+     * @param permission The name of the permission being checked.
+     * @param targetUserId The userId of the user which the caller needs permission to act on.
+     * @throws SecurityException if the caller has not been granted the given permission,
+     * the associtated cross-user permission if the caller's user is different to the target user.
+     */
+    private void enforcePermission(String permission, int targetUserId)
+            throws SecurityException {
+        if (!hasPermission(permission, targetUserId)) {
+            throw new SecurityException("Caller does not have the required permissions for "
+                    + "this user. Permissions required: {"
+                    + permission
+                    + ", "
+                    + CROSS_USER_PERMISSIONS.get(permission)
+                    + "}");
+        }
+    }
+
+    /**
+     * Return whether the calling process has been granted permission to query a device policy on
+     * a specific user.
+     *
+     * @param permission The name of the permission being checked.
+     * @param targetUserId The userId of the user which the caller needs permission to act on.
+     * @throws SecurityException if the caller has not been granted the given permission,
+     * the associatated cross-user permission if the caller's user is different to the target user
+     * and if the user has not been granted {@link QUERY_ADMIN_POLICY}.
+     */
+    private void enforceCanQuery(String permission, int targetUserId) throws SecurityException {
+        if (hasPermission(QUERY_ADMIN_POLICY)) {
+            return;
+        }
+        enforcePermission(permission, targetUserId);
+    }
+
+    /**
+     * Return whether the calling process has been granted permission to apply a device policy on
+     * a specific user.
+     *
+     * @param permission The name of the permission being checked.
+     * @param targetUserId The userId of the user which the caller needs permission to act on.
+     */
+    private boolean hasPermission(String permission, int targetUserId) {
+        boolean hasPermissionOnOwnUser = hasPermission(permission);
+        boolean hasPermissionOnTargetUser = true;
+        if (hasPermissionOnOwnUser & getCallerIdentity().getUserId() != targetUserId) {
+            hasPermissionOnTargetUser = hasPermission(CROSS_USER_PERMISSIONS.get(permission));
+        }
+        return hasPermissionOnOwnUser && hasPermissionOnTargetUser;
+    }
+
+    /**
+     * Return whether the calling process has been granted the given permission.
+     *
+     * @param permission The name of the permission being checked.
+     */
+    private boolean hasPermission(String permission) {
+        if (permission == null) {
+            return true;
+        }
+
+        CallerIdentity caller = getCallerIdentity();
+
+        // Check if the caller holds the permission
+        if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+            return true;
+        }
+        // Check the permissions of DPCs
+        if (isDefaultDeviceOwner(caller)) {
+            return DPC_PERMISSIONS.get(DEFAULT_DEVICE_OWNER).contains(permission);
+        }
+        if (isFinancedDeviceOwner(caller)) {
+            return DPC_PERMISSIONS.get(FINANCED_DEVICE_OWNER).contains(permission);
+        }
+        if (isProfileOwnerOfOrganizationOwnedDevice(caller)) {
+            return DPC_PERMISSIONS.get(PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE).contains(
+                    permission);
+        }
+        if (isProfileOwnerOnUser0(caller)) {
+            return DPC_PERMISSIONS.get(PROFILE_OWNER_ON_USER_0).contains(permission);
+        }
+        if (isProfileOwner(caller)) {
+            return DPC_PERMISSIONS.get(PROFILE_OWNER).contains(permission);
+        }
+        // Check the permission for the role-holder
+        if (isDevicePolicyManagementRoleHolder(caller)) {
+            return anyDpcHasPermission(permission, mContext.getUserId());
+        }
+        // Check if the caller is an active admin that uses a certain policy.
+        if (ACTIVE_ADMIN_POLICIES.containsKey(permission)) {
+            return getActiveAdminForCallerLocked(
+                    null, ACTIVE_ADMIN_POLICIES.get(permission), false) != null;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns whether there is a DPC on the given user that has been granted the given permission.
+     *
+     * @param permission The name of the permission being checked.
+     * @param userId The id of the user to check.
+     */
+    private boolean anyDpcHasPermission(String permission, int userId) {
+        if (mOwners.isDefaultDeviceOwnerUserId(userId)) {
+            return DPC_PERMISSIONS.get(DEFAULT_DEVICE_OWNER).contains(permission);
+        }
+        if (mOwners.isFinancedDeviceOwnerUserId(userId)) {
+            return DPC_PERMISSIONS.get(FINANCED_DEVICE_OWNER).contains(permission);
+        }
+        if (mOwners.isProfileOwnerOfOrganizationOwnedDevice(userId)) {
+            return DPC_PERMISSIONS.get(PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE).contains(
+                    permission);
+        }
+        if (userId == 0 && mOwners.hasProfileOwner(0)) {
+            return DPC_PERMISSIONS.get(PROFILE_OWNER_ON_USER_0).contains(permission);
+        }
+        if (mOwners.hasProfileOwner(userId)) {
+            return DPC_PERMISSIONS.get(PROFILE_OWNER).contains(permission);
+        }
+        return false;
+    }
+
+    private boolean isPermissionCheckFlagEnabled() {
+        return DeviceConfig.getBoolean(
+                NAMESPACE_DEVICE_POLICY_MANAGER,
+                PERMISSION_BASED_ACCESS_EXPERIMENT_FLAG,
+                DEFAULT_VALUE_PERMISSION_BASED_ACCESS_FLAG);
+    }
+
     // TODO(b/260560985): properly gate coexistence changes
     private boolean isCoexistenceEnabled(CallerIdentity caller) {
         return isCoexistenceFlagEnabled()
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
index 6f172e4..581a199 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
@@ -19,6 +19,7 @@
 import static android.app.admin.DevicePolicyManager.DEPRECATE_USERMANAGERINTERNAL_DEVICEPOLICY_DEFAULT;
 import static android.app.admin.DevicePolicyManager.DEPRECATE_USERMANAGERINTERNAL_DEVICEPOLICY_FLAG;
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT;
+import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
 
 import static com.android.server.devicepolicy.DeviceStateCacheImpl.NO_DEVICE_OWNER;
 
@@ -455,6 +456,23 @@
         }
     }
 
+    boolean isDefaultDeviceOwnerUserId(int userId) {
+        synchronized (mData) {
+            return mData.mDeviceOwner != null
+                    && mData.mDeviceOwnerUserId == userId
+                    && getDeviceOwnerType(getDeviceOwnerPackageName()) == DEVICE_OWNER_TYPE_DEFAULT;
+        }
+    }
+
+    boolean isFinancedDeviceOwnerUserId(int userId) {
+        synchronized (mData) {
+            return mData.mDeviceOwner != null
+                    && mData.mDeviceOwnerUserId == userId
+                    && getDeviceOwnerType(getDeviceOwnerPackageName())
+                        == DEVICE_OWNER_TYPE_FINANCED;
+        }
+    }
+
     boolean hasProfileOwner(int userId) {
         synchronized (mData) {
             return getProfileOwnerComponent(userId) != null;
diff --git a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
index e0812d6..73ddbe8c 100644
--- a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
@@ -78,6 +78,7 @@
 import org.robolectric.shadows.ShadowPackageManager;
 
 import java.util.ArrayDeque;
+import java.util.Arrays;
 
 @RunWith(RobolectricTestRunner.class)
 @Config(
@@ -196,7 +197,7 @@
         mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
         TransportMock transportMock = setUpTransport(mTransport);
         when(transportMock.transport.getAvailableRestoreSets())
-                .thenReturn(new RestoreSet[] {mRestoreSet1, mRestoreSet2});
+                .thenReturn(Arrays.asList(mRestoreSet1, mRestoreSet2));
         IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
 
         int result = restoreSession.getAvailableRestoreSets(mObserver, mMonitor);
@@ -214,7 +215,8 @@
     public void testGetAvailableRestoreSets_forEmptyRestoreSets() throws Exception {
         mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP);
         TransportMock transportMock = setUpTransport(mTransport);
-        when(transportMock.transport.getAvailableRestoreSets()).thenReturn(new RestoreSet[0]);
+        when(transportMock.transport.getAvailableRestoreSets()).thenReturn(
+                Arrays.asList(new RestoreSet[0]));
         IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport);
 
         int result = restoreSession.getAvailableRestoreSets(mObserver, mMonitor);
@@ -593,7 +595,7 @@
                 new ActiveRestoreSession(
                         mBackupManagerService, packageName, transport.transportName,
                         mBackupEligibilityRules);
-        restoreSession.setRestoreSets(restoreSets);
+        restoreSession.setRestoreSets(Arrays.asList(restoreSets));
         return restoreSession;
     }
 
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
index 1619856..f0e3f3f 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
@@ -89,6 +89,10 @@
         "sortReceivers",
         "sortServices",
         "setAllComponentsDirectBootAware",
+        "getUsesLibrariesSorted",
+        "getUsesOptionalLibrariesSorted",
+        "getUsesSdkLibrariesSorted",
+        "getUsesStaticLibrariesSorted",
         // Tested through setting minor/major manually
         "setLongVersionCode",
         "getLongVersionCode",
diff --git a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/OWNERS b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/OWNERS
new file mode 100644
index 0000000..2e475a9
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/OWNERS
@@ -0,0 +1 @@
+include /services/companion/java/com/android/server/companion/virtual/OWNERS
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java b/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
index ea04a19..5b10dc4 100644
--- a/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
@@ -35,6 +35,7 @@
 
 import com.android.server.testutils.OffsettableClock;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -73,6 +74,15 @@
         );
     }
 
+    @After
+    public void tearDown() {
+        if (mController != null) {
+            // Stop the update Brightness loop.
+            mController.stop();
+            mController = null;
+        }
+    }
+
     @Test
     public void testBrightness() throws Exception {
         when(mSensorManager.registerListener(any(SensorEventListener.class), eq(mLightSensor),
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index 1c44da1..5560b41 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -6,6 +6,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 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.assertThrows;
@@ -208,6 +209,34 @@
         assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size());
     }
 
+    /**
+     * Test that dynamic constraints aren't written to disk.
+     */
+    @Test
+    public void testDynamicConstraintsNotPersisted() throws Exception {
+        JobInfo.Builder b = new Builder(42, mComponent).setPersisted(true);
+        JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null);
+        js.addDynamicConstraints(JobStatus.CONSTRAINT_BATTERY_NOT_LOW
+                | JobStatus.CONSTRAINT_CHARGING
+                | JobStatus.CONSTRAINT_IDLE
+                | JobStatus.CONSTRAINT_STORAGE_NOT_LOW);
+        assertTrue(js.hasBatteryNotLowConstraint());
+        assertTrue(js.hasChargingConstraint());
+        assertTrue(js.hasIdleConstraint());
+        assertTrue(js.hasStorageNotLowConstraint());
+        mTaskStoreUnderTest.add(js);
+        waitForPendingIo();
+
+        final JobSet jobStatusSet = new JobSet();
+        mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+        assertEquals("Job count is incorrect.", 1, jobStatusSet.size());
+        JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+        assertFalse(loaded.hasBatteryNotLowConstraint());
+        assertFalse(loaded.hasChargingConstraint());
+        assertFalse(loaded.hasIdleConstraint());
+        assertFalse(loaded.hasStorageNotLowConstraint());
+    }
+
     @Test
     public void testExtractUidFromJobFileName() {
         File file = new File(mTestContext.getFilesDir(), "randomName");
diff --git a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
index 3268df2..8c3838b 100644
--- a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
@@ -496,8 +496,10 @@
         Random random = new Random(1); // Always use the same series of pseudo random values.
 
         for (int i = 0; i < 5000; ++i) {
+            final boolean ui = random.nextBoolean();
+            final boolean ej = !ui && random.nextBoolean();
             JobStatus job = createJobStatus("testPendingJobSorting_Random",
-                    createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(250));
+                    createJobInfo(i).setExpedited(ej).setUserInitiated(ui), random.nextInt(250));
             job.enqueueTime = random.nextInt(1_000_000);
             jobQueue.add(job);
         }
@@ -531,8 +533,10 @@
             jobQueue.clear();
 
             for (int i = 0; i < 300; ++i) {
+                final boolean ui = random.nextBoolean();
+                final boolean ej = !ui && random.nextBoolean();
                 JobStatus job = createJobStatus("testPendingJobSortingTransitivity",
-                        createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(50));
+                        createJobInfo(i).setExpedited(ej).setUserInitiated(ui), random.nextInt(50));
                 job.enqueueTime = random.nextInt(1_000_000);
                 job.overrideState = random.nextInt(4);
                 jobQueue.add(job);
@@ -553,8 +557,10 @@
             jobQueue.clear();
 
             for (int i = 0; i < 300; ++i) {
+                final boolean ui = random.nextFloat() < .02;
+                final boolean ej = !ui && random.nextFloat() < .03;
                 JobStatus job = createJobStatus("testPendingJobSortingTransitivity_Concentrated",
-                        createJobInfo(i).setExpedited(random.nextFloat() < .03),
+                        createJobInfo(i).setExpedited(ej).setUserInitiated(ui),
                         random.nextInt(20));
                 job.enqueueTime = random.nextInt(250);
                 job.overrideState = random.nextFloat() < .01
@@ -653,10 +659,11 @@
     }
 
     private void checkPendingJobInvariants(PendingJobQueue jobQueue) {
+        final SparseBooleanArray eJobSeen = new SparseBooleanArray();
         final SparseBooleanArray regJobSeen = new SparseBooleanArray();
         // Latest priority enqueue times seen for each priority+namespace for each app.
         final SparseArrayMap<String, SparseLongArray> latestPriorityRegEnqueueTimesPerUid =
-                new SparseArrayMap();
+                new SparseArrayMap<>();
         final SparseArrayMap<String, SparseLongArray> latestPriorityEjEnqueueTimesPerUid =
                 new SparseArrayMap<>();
         final int noEntry = -1;
@@ -672,7 +679,8 @@
             // Invariant #1: All jobs are sorted by override state
             // Invariant #2: All jobs (for a UID) are sorted by priority order
             // Invariant #3: Jobs (for a UID) with the same priority are sorted by enqueue time.
-            // Invariant #4: EJs (for a UID) should be before regular jobs
+            // Invariant #4: User-initiated jobs (for a UID) should be before all other jobs.
+            // Invariant #5: EJs (for a UID) should be before regular jobs
 
             // Invariant 1
             if (prevOverrideState != job.overrideState) {
@@ -683,6 +691,7 @@
                 // to avoid confusion in the other checks.
                 latestPriorityEjEnqueueTimesPerUid.clear();
                 latestPriorityRegEnqueueTimesPerUid.clear();
+                eJobSeen.clear();
                 regJobSeen.clear();
                 prevOverrideState = job.overrideState;
             }
@@ -718,10 +727,24 @@
             }
             latestPriorityEnqueueTimes.put(priority, job.enqueueTime);
 
-            // Invariant 4
-            if (!job.isRequestedExpeditedJob()) {
+            if (job.isRequestedExpeditedJob()) {
+                eJobSeen.put(uid, true);
+            } else if (!job.getJob().isUserInitiated()) {
                 regJobSeen.put(uid, true);
-            } else if (regJobSeen.get(uid)) {
+            }
+
+            // Invariant 4
+            if (job.getJob().isUserInitiated()) {
+                if (eJobSeen.get(uid)) {
+                    fail("UID " + uid + " had a UIJ ordered after an EJ");
+                }
+                if (regJobSeen.get(uid)) {
+                    fail("UID " + uid + " had a UIJ ordered after a regular job");
+                }
+            }
+
+            // Invariant 5
+            if (job.isRequestedExpeditedJob() && regJobSeen.get(uid)) {
                 fail("UID " + uid + " had an EJ ordered after a regular job");
             }
         }
@@ -733,6 +756,9 @@
                 + "/o" + job.overrideState
                 + "/p" + job.getEffectivePriority()
                 + "/b" + job.lastEvaluatedBias
-                + "/" + job.isRequestedExpeditedJob() + "@" + job.enqueueTime;
+                + "/"
+                + (job.isRequestedExpeditedJob()
+                        ? "e" : (job.getJob().isUserInitiated() ? "u" : "r"))
+                + "@" + job.enqueueTime;
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
new file mode 100644
index 0000000..6d778afe
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -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.server.wm;
+
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.Property;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+ /**
+ * Test class for {@link LetterboxUiControllerTest}.
+ *
+ * Build/Install/Run:
+ *  atest WmTests:LetterboxUiControllerTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class LetterboxUiControllerTest extends WindowTestsBase {
+
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+    private ActivityRecord mActivity;
+    private DisplayContent mDisplayContent;
+    private LetterboxUiController mController;
+    private LetterboxConfiguration mLetterboxConfiguration;
+
+    @Before
+    public void setUp() throws Exception {
+        mActivity = setUpActivityWithComponent();
+
+        mLetterboxConfiguration = mWm.mLetterboxConfiguration;
+        spyOn(mLetterboxConfiguration);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
+        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
+
+        assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
+        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+
+        // Recreate DisplayContent with DisplayRotationCompatPolicy
+        mActivity = setUpActivityWithComponent();
+        mController = new LetterboxUiController(mWm, mActivity);
+        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
+        mController.setRelauchingAfterRequestedOrientationChanged(false);
+
+        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
+        doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
+                .isTreatmentEnabledForActivity(eq(mActivity));
+
+        assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
+    }
+
+    @Test
+    public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() {
+        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
+
+        assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
+    }
+
+    @Test
+    public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isPolicyForIgnoringRequestedOrientationEnabled();
+        mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ true);
+        mController = new LetterboxUiController(mWm, mActivity);
+        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
+
+        assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isPolicyForIgnoringRequestedOrientationEnabled();
+        mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
+
+        assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
+        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
+        doReturn(false).when(mLetterboxConfiguration)
+                .isPolicyForIgnoringRequestedOrientationEnabled();
+
+        assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
+    }
+
+    private void mockThatProperty(String propertyName, boolean value) throws Exception {
+        Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
+                 /* className */ "");
+        PackageManager pm = mWm.mContext.getPackageManager();
+        spyOn(pm);
+        doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
+    }
+
+    private void prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch() {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isPolicyForIgnoringRequestedOrientationEnabled();
+        mController.setRelauchingAfterRequestedOrientationChanged(true);
+    }
+
+    private ActivityRecord setUpActivityWithComponent() {
+        mDisplayContent = new TestDisplayContent
+                .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+        Task task = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build();
+        final ActivityRecord activity = new ActivityBuilder(mAtm)
+                .setOnTop(true)
+                .setTask(task)
+                // Set the component to be that of the test class in order to enable compat changes
+                .setComponent(ComponentName.createRelative(mContext,
+                        com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+                .build();
+        return activity;
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 367f91b..d364dbb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -30,7 +30,6 @@
 import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
 import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.os.Process.NOBODY_UID;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -1208,34 +1207,20 @@
 
     @Test
     public void testCreateRecentTaskInfo_detachedTask() {
-        final Task task = createTaskBuilder(".Task").build();
-        new ActivityBuilder(mSupervisor.mService)
-                .setTask(task)
-                .setUid(NOBODY_UID)
-                .setComponent(getUniqueComponentName())
-                .build();
+        final Task task = createTaskBuilder(".Task").setCreateActivity(true).build();
         final TaskDisplayArea tda = task.getDisplayArea();
 
         assertTrue(task.isAttached());
         assertTrue(task.supportsMultiWindow());
 
-        RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
-                true /* getTasksAllowed */);
+        RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true);
 
         assertTrue(info.supportsMultiWindow);
 
-        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
-                false /* getTasksAllowed */);
-
-        assertTrue(info.topActivity == null);
-        assertTrue(info.topActivityInfo == null);
-        assertTrue(info.baseActivity == null);
-
         // The task can be put in split screen even if it is not attached now.
         task.removeImmediately();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
-                true /* getTasksAllowed */);
+        info = mRecentTasks.createRecentTaskInfo(task, true);
 
         assertTrue(info.supportsMultiWindow);
 
@@ -1244,8 +1229,7 @@
         doReturn(false).when(tda).supportsNonResizableMultiWindow();
         doReturn(false).when(task).isResizeable();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
-                true /* getTasksAllowed */);
+        info = mRecentTasks.createRecentTaskInfo(task, true);
 
         assertFalse(info.supportsMultiWindow);
 
@@ -1253,8 +1237,7 @@
         // the device supports it.
         doReturn(true).when(tda).supportsNonResizableMultiWindow();
 
-        info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
-                true /* getTasksAllowed */);
+        info = mRecentTasks.createRecentTaskInfo(task, true);
 
         assertTrue(info.supportsMultiWindow);
     }
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 2cce62e..a48a0bc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -94,6 +94,7 @@
 import android.provider.DeviceConfig.Properties;
 import android.view.InsetsFrameProvider;
 import android.view.InsetsSource;
+import android.view.InsetsState;
 import android.view.WindowManager;
 
 import androidx.test.filters.MediumTest;
@@ -1466,6 +1467,12 @@
         assertEquals(new Rect(notchHeight, 0, 0, 0), mActivity.getLetterboxInsets());
         assertTrue(displayPolicy.isFullyTransparentAllowed(w, ITYPE_STATUS_BAR));
         assertActivityMaxBoundsSandboxed();
+
+        // The insets state for metrics should be rotated (landscape).
+        final InsetsState insetsState = new InsetsState();
+        mActivity.mDisplayContent.getInsetsPolicy().getInsetsForWindowMetrics(
+                mActivity, insetsState);
+        assertEquals(dh, insetsState.getDisplayFrame().width());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 26fe521..b70d8bd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -23,6 +23,7 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
 import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE;
 import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE;
 import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE;
@@ -73,6 +74,7 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Binder;
@@ -83,8 +85,10 @@
 import android.view.RemoteAnimationDefinition;
 import android.view.SurfaceControl;
 import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentCreationParams;
 import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentOperation;
 import android.window.TaskFragmentOrganizer;
 import android.window.TaskFragmentOrganizerToken;
 import android.window.TaskFragmentParentInfo;
@@ -689,6 +693,59 @@
     }
 
     @Test
+    public void testApplyTransaction_enforceTaskFragmentOrganized_setTaskFragmentOperation() {
+        final Task task = createTask(mDisplayContent);
+        mTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setFragmentToken(mFragmentToken)
+                .build();
+        mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
+        final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+                OP_TYPE_SET_ANIMATION_PARAMS)
+                .setAnimationParams(TaskFragmentAnimationParams.DEFAULT)
+                .build();
+        mTransaction.setTaskFragmentOperation(mFragmentToken, operation);
+        mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE,
+                false /* shouldApplyIndependently */);
+
+        // Not allowed because TaskFragment is not organized by the caller organizer.
+        assertApplyTransactionDisallowed(mTransaction);
+
+        mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */,
+                "Test:TaskFragmentOrganizer" /* processName */);
+
+        assertApplyTransactionAllowed(mTransaction);
+    }
+
+    @Test
+    public void testSetTaskFragmentOperation() {
+        final Task task = createTask(mDisplayContent);
+        mTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(mFragmentToken)
+                .build();
+        assertEquals(TaskFragmentAnimationParams.DEFAULT, mTaskFragment.getAnimationParams());
+
+        mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
+        final TaskFragmentAnimationParams animationParams =
+                new TaskFragmentAnimationParams.Builder()
+                        .setAnimationBackgroundColor(Color.GREEN)
+                        .build();
+        final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+                OP_TYPE_SET_ANIMATION_PARAMS)
+                .setAnimationParams(animationParams)
+                .build();
+        mTransaction.setTaskFragmentOperation(mFragmentToken, operation);
+        mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE,
+                false /* shouldApplyIndependently */);
+        assertApplyTransactionAllowed(mTransaction);
+
+        assertEquals(animationParams, mTaskFragment.getAnimationParams());
+        assertEquals(Color.GREEN, mTaskFragment.getAnimationParams().getAnimationBackgroundColor());
+    }
+
+    @Test
     public void testApplyTransaction_createTaskFragment_failForDifferentUid() {
         final ActivityRecord activity = createActivityRecord(mDisplayContent);
         final int uid = Binder.getCallingUid();
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
similarity index 95%
rename from services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java
rename to services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index 689423a..4d6d320 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -93,21 +93,27 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * A class that provides trusted hotword detector to communicate with the {@link
- * HotwordDetectionService}.
+ * A class that provides sandboxed detector to communicate with the {@link
+ * HotwordDetectionService} and {@link VisualQueryDetectionService}.
  *
- * This class provides the methods to do initialization with the {@link HotwordDetectionService}
- * and handle external source detection. It also provides the methods to check if we can egress
- * the data from the {@link HotwordDetectionService}.
+ * Trusted hotword detectors such as {@link SoftwareHotwordDetector} and
+ * {@link AlwaysOnHotwordDetector} will leverage this class to communitcate with
+ * {@link HotwordDetectionService}; similarly, {@link VisualQueryDetector} will communicate with
+ * {@link VisualQueryDetectionService}.
+ *
+ * This class provides the methods to do initialization with the {@link HotwordDetectionService} and
+ * {@link VisualQueryDetectionService} handles external source detection for
+ * {@link HotwordDetectionService}. It also provides the methods to check if we can egress the data
+ * from the {@link HotwordDetectionService} and {@link VisualQueryDetectionService}.
  *
  * The subclass should override the {@link #informRestartProcessLocked()} to handle the trusted
  * process restart.
  */
-abstract class HotwordDetectorSession {
-    private static final String TAG = "HotwordDetectorSession";
+abstract class DetectorSession {
+    private static final String TAG = "DetectorSession";
     static final boolean DEBUG = false;
 
-    private static final String OP_MESSAGE =
+    private static final String HOTWORD_DETECTION_OP_MESSAGE =
             "Providing hotword detection result to VoiceInteractionService";
 
     // The error codes are used for onError callback
@@ -173,7 +179,7 @@
     @GuardedBy("mLock")
     ParcelFileDescriptor mCurrentAudioSink;
     @GuardedBy("mLock")
-    @NonNull HotwordDetectionConnection.ServiceConnection mRemoteHotwordDetectionService;
+    @NonNull HotwordDetectionConnection.ServiceConnection mRemoteDetectionService;
     boolean mDebugHotwordLogging = false;
     @GuardedBy("mLock")
     private double mProximityMeters = PROXIMITY_UNKNOWN;
@@ -185,13 +191,13 @@
     boolean mPerformingExternalSourceHotwordDetection;
     @NonNull final IBinder mToken;
 
-    HotwordDetectorSession(
-            @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService,
+    DetectorSession(
+            @NonNull HotwordDetectionConnection.ServiceConnection remoteDetectionService,
             @NonNull Object lock, @NonNull Context context, @NonNull IBinder token,
             @NonNull IHotwordRecognitionStatusCallback callback, int voiceInteractionServiceUid,
             Identity voiceInteractorIdentity,
             @NonNull ScheduledExecutorService scheduledExecutorService, boolean logging) {
-        mRemoteHotwordDetectionService = remoteHotwordDetectionService;
+        mRemoteDetectionService = remoteDetectionService;
         mLock = lock;
         mContext = context;
         mToken = token;
@@ -219,7 +225,7 @@
         if (DEBUG) {
             Slog.d(TAG, "updateStateAfterProcessStartLocked");
         }
-        AndroidFuture<Void> voidFuture = mRemoteHotwordDetectionService.postAsync(service -> {
+        AndroidFuture<Void> voidFuture = mRemoteDetectionService.postAsync(service -> {
             AndroidFuture<Void> future = new AndroidFuture<>();
             IRemoteCallback statusCallback = new IRemoteCallback.Stub() {
                 @Override
@@ -319,7 +325,7 @@
             Slog.v(TAG, "call updateStateAfterProcessStartLocked");
             updateStateAfterProcessStartLocked(options, sharedMemory);
         } else {
-            mRemoteHotwordDetectionService.run(
+            mRemoteDetectionService.run(
                     service -> service.updateState(options, sharedMemory, /* callback= */ null));
         }
     }
@@ -407,7 +413,7 @@
 
         // TODO: handle cancellations well
         // TODO: what if we cancelled and started a new one?
-        mRemoteHotwordDetectionService.run(
+        mRemoteDetectionService.run(
                 service -> {
                     service.detectFromMicrophoneSource(
                             serviceAudioSource,
@@ -512,7 +518,7 @@
     void destroyLocked() {
         mDestroyed = true;
         mDebugHotwordLogging = false;
-        mRemoteHotwordDetectionService = null;
+        mRemoteDetectionService = null;
         if (mAttentionManagerInternal != null) {
             mAttentionManagerInternal.onStopProximityUpdates(mProximityCallbackInternal);
         }
@@ -524,9 +530,9 @@
     }
 
     @SuppressWarnings("GuardedBy")
-    void updateRemoteHotwordDetectionServiceLocked(
-            @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService) {
-        mRemoteHotwordDetectionService = remoteHotwordDetectionService;
+    void updateRemoteSandboxedDetectionServiceLocked(
+            @NonNull HotwordDetectionConnection.ServiceConnection remoteDetectionService) {
+        mRemoteDetectionService = remoteDetectionService;
     }
 
     void reportErrorLocked(int status) {
@@ -628,9 +634,9 @@
                 int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
                 mAppOpsManager.noteOpNoThrow(hotwordOp,
                         mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
-                        mVoiceInteractorIdentity.attributionTag, OP_MESSAGE);
+                        mVoiceInteractorIdentity.attributionTag, HOTWORD_DETECTION_OP_MESSAGE);
                 enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
-                        CAPTURE_AUDIO_HOTWORD, OP_MESSAGE);
+                        CAPTURE_AUDIO_HOTWORD, HOTWORD_DETECTION_OP_MESSAGE);
             }
         });
     }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
index 84bd7160..ad84f00 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
@@ -32,7 +32,6 @@
 import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.SharedMemory;
-import android.service.voice.AlwaysOnHotwordDetector;
 import android.service.voice.HotwordDetectedResult;
 import android.service.voice.HotwordDetectionService;
 import android.service.voice.HotwordDetector;
@@ -59,7 +58,7 @@
  * {@link android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector(String,
  * Locale, PersistableBundle, SharedMemory, AlwaysOnHotwordDetector.Callback)}.
  */
-final class DspTrustedHotwordDetectorSession extends HotwordDetectorSession {
+final class DspTrustedHotwordDetectorSession extends DetectorSession {
     private static final String TAG = "DspTrustedHotwordDetectorSession";
 
     // The validation timeout value is 3 seconds for onDetect of DSP trigger event.
@@ -182,7 +181,7 @@
         };
 
         mValidatingDspTrigger = true;
-        mRemoteHotwordDetectionService.run(service -> {
+        mRemoteDetectionService.run(service -> {
             // We use the VALIDATION_TIMEOUT_MILLIS to inform that the client needs to invoke
             // the callback before timeout value. In order to reduce the latency impact between
             // server side and client side, we need to use another timeout value
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 276bccd..d501af7 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -89,7 +89,7 @@
             Executors.newSingleThreadScheduledExecutor();
     @Nullable private final ScheduledFuture<?> mCancellationTaskFuture;
     private final IBinder.DeathRecipient mAudioServerDeathRecipient = this::audioServerDied;
-    @NonNull private final ServiceConnectionFactory mServiceConnectionFactory;
+    @NonNull private final ServiceConnectionFactory mHotwordDetectionServiceConnectionFactory;
     private final int mDetectorType;
     /**
      * Time after which each HotwordDetectionService process is stopped and replaced by a new one.
@@ -99,7 +99,7 @@
 
     final Object mLock;
     final int mVoiceInteractionServiceUid;
-    final ComponentName mDetectionComponentName;
+    final ComponentName mHotwordDetectionComponentName;
     final int mUser;
     final Context mContext;
     volatile HotwordDetectionServiceIdentity mIdentity;
@@ -122,27 +122,30 @@
      * to record the detectors.
      */
     @GuardedBy("mLock")
-    private final SparseArray<HotwordDetectorSession> mHotwordDetectorSessions =
+    private final SparseArray<DetectorSession> mDetectorSessions =
             new SparseArray<>();
 
     HotwordDetectionConnection(Object lock, Context context, int voiceInteractionServiceUid,
-            Identity voiceInteractorIdentity, ComponentName serviceName, int userId,
+            Identity voiceInteractorIdentity, ComponentName hotwordDetectionServiceName, int userId,
             boolean bindInstantServiceAllowed, int detectorType) {
         mLock = lock;
         mContext = context;
         mVoiceInteractionServiceUid = voiceInteractionServiceUid;
         mVoiceInteractorIdentity = voiceInteractorIdentity;
-        mDetectionComponentName = serviceName;
+        mHotwordDetectionComponentName = hotwordDetectionServiceName;
         mUser = userId;
         mDetectorType = detectorType;
         mReStartPeriodSeconds = DeviceConfig.getInt(DeviceConfig.NAMESPACE_VOICE_INTERACTION,
                 KEY_RESTART_PERIOD_IN_SECONDS, 0);
-        final Intent intent = new Intent(HotwordDetectionService.SERVICE_INTERFACE);
-        intent.setComponent(mDetectionComponentName);
+        final Intent hotwordDetectionServiceIntent =
+                new Intent(HotwordDetectionService.SERVICE_INTERFACE);
+        hotwordDetectionServiceIntent.setComponent(mHotwordDetectionComponentName);
         initAudioFlingerLocked();
 
-        mServiceConnectionFactory = new ServiceConnectionFactory(intent, bindInstantServiceAllowed);
-        mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked();
+        mHotwordDetectionServiceConnectionFactory =
+                new ServiceConnectionFactory(hotwordDetectionServiceIntent,
+                        bindInstantServiceAllowed);
+        mRemoteHotwordDetectionService = mHotwordDetectionServiceConnectionFactory.createLocked();
         mLastRestartInstant = Instant.now();
 
         if (mReStartPeriodSeconds <= 0) {
@@ -176,8 +179,8 @@
         try {
             mAudioFlinger.linkToDeath(mAudioServerDeathRecipient, /* flags= */ 0);
         } catch (RemoteException e) {
-            Slog.w(TAG, "Audio server died before we registered a DeathRecipient; retrying init.",
-                    e);
+            Slog.w(TAG, "Audio server died before we registered a DeathRecipient; "
+                            + "retrying init.", e);
             initAudioFlingerLocked();
         }
     }
@@ -200,10 +203,10 @@
     void cancelLocked() {
         Slog.v(TAG, "cancelLocked");
         clearDebugHotwordLoggingTimeoutLocked();
-        runForEachHotwordDetectorSessionLocked((session) -> {
+        runForEachDetectorSessionLocked((session) -> {
             session.destroyLocked();
         });
-        mHotwordDetectorSessions.clear();
+        mDetectorSessions.clear();
         mDebugHotwordLogging = false;
         mRemoteHotwordDetectionService.unbind();
         LocalServices.getService(PermissionManagerServiceInternal.class)
@@ -223,7 +226,7 @@
     @SuppressWarnings("GuardedBy")
     void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory,
             @NonNull IBinder token) {
-        final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token);
+        final DetectorSession session = getDetectorSessionByTokenLocked(token);
         if (session == null) {
             Slog.v(TAG, "Not found the detector by token");
             return;
@@ -259,7 +262,7 @@
         if (DEBUG) {
             Slog.d(TAG, "startListeningFromExternalSourceLocked");
         }
-        final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token);
+        final DetectorSession session = getDetectorSessionByTokenLocked(token);
         if (session == null) {
             Slog.v(TAG, "Not found the detector by token");
             return;
@@ -321,7 +324,7 @@
         Slog.v(TAG, "setDebugHotwordLoggingLocked: " + logging);
         clearDebugHotwordLoggingTimeoutLocked();
         mDebugHotwordLogging = logging;
-        runForEachHotwordDetectorSessionLocked((session) -> {
+        runForEachDetectorSessionLocked((session) -> {
             session.setDebugHotwordLoggingLocked(logging);
         });
 
@@ -331,7 +334,7 @@
                 Slog.v(TAG, "Timeout to reset mDebugHotwordLogging to false");
                 synchronized (mLock) {
                     mDebugHotwordLogging = false;
-                    runForEachHotwordDetectorSessionLocked((session) -> {
+                    runForEachDetectorSessionLocked((session) -> {
                         session.setDebugHotwordLoggingLocked(false);
                     });
                 }
@@ -350,24 +353,24 @@
     private void restartProcessLocked() {
         // TODO(b/244598068): Check HotwordAudioStreamManager first
         Slog.v(TAG, "Restarting hotword detection process");
-        ServiceConnection oldConnection = mRemoteHotwordDetectionService;
+        ServiceConnection oldHotwordConnection = mRemoteHotwordDetectionService;
         HotwordDetectionServiceIdentity previousIdentity = mIdentity;
 
         mLastRestartInstant = Instant.now();
         // Recreate connection to reset the cache.
-        mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked();
+        mRemoteHotwordDetectionService = mHotwordDetectionServiceConnectionFactory.createLocked();
 
         Slog.v(TAG, "Started the new process, dispatching processRestarted to detector");
-        runForEachHotwordDetectorSessionLocked((session) -> {
-            session.updateRemoteHotwordDetectionServiceLocked(mRemoteHotwordDetectionService);
+        runForEachDetectorSessionLocked((session) -> {
+            session.updateRemoteSandboxedDetectionServiceLocked(mRemoteHotwordDetectionService);
             session.informRestartProcessLocked();
         });
         if (DEBUG) {
             Slog.i(TAG, "processRestarted is dispatched done, unbinding from the old process");
         }
 
-        oldConnection.ignoreConnectionStatusEvents();
-        oldConnection.unbind();
+        oldHotwordConnection.ignoreConnectionStatusEvents();
+        oldHotwordConnection.unbind();
         if (previousIdentity != null) {
             removeServiceUidForAudioPolicy(previousIdentity.getIsolatedUid());
         }
@@ -437,12 +440,12 @@
             pw.print(prefix); pw.print("mBound=");
             pw.println(mRemoteHotwordDetectionService.isBound());
             pw.print(prefix); pw.print("mRestartCount=");
-            pw.println(mServiceConnectionFactory.mRestartCount);
+            pw.println(mHotwordDetectionServiceConnectionFactory.mRestartCount);
             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(s)");
-            runForEachHotwordDetectorSessionLocked((session) -> {
+            pw.print(prefix); pw.println("DetectorSession(s)");
+            runForEachDetectorSessionLocked((session) -> {
                 session.dumpLocked(prefix, pw);
             });
         }
@@ -537,9 +540,9 @@
                 }
             }
             synchronized (HotwordDetectionConnection.this.mLock) {
-                runForEachHotwordDetectorSessionLocked((session) -> {
+                runForEachDetectorSessionLocked((session) -> {
                     session.reportErrorLocked(
-                            HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED);
+                            DetectorSession.HOTWORD_DETECTION_SERVICE_DIED);
                 });
             }
             // Can improve to log exit reason if needed
@@ -599,12 +602,12 @@
             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);
+        DetectorSession removeSession = mDetectorSessions.get(detectorType);
         if (removeSession != null) {
             removeSession.destroyLocked();
-            mHotwordDetectorSessions.remove(detectorType);
+            mDetectorSessions.remove(detectorType);
         }
-        final HotwordDetectorSession session;
+        final DetectorSession session;
         if (detectorType == HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP) {
             session = new DspTrustedHotwordDetectorSession(mRemoteHotwordDetectionService,
                     mLock, mContext, token, callback, mVoiceInteractionServiceUid,
@@ -615,30 +618,30 @@
                     mVoiceInteractionServiceUid, mVoiceInteractorIdentity,
                     mScheduledExecutorService, mDebugHotwordLogging);
         }
-        mHotwordDetectorSessions.put(detectorType, session);
+        mDetectorSessions.put(detectorType, session);
         session.initialize(options, sharedMemory);
     }
 
     @SuppressWarnings("GuardedBy")
     void destroyDetectorLocked(@NonNull IBinder token) {
-        final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token);
+        final DetectorSession session = getDetectorSessionByTokenLocked(token);
         if (session != null) {
             session.destroyLocked();
-            final int index = mHotwordDetectorSessions.indexOfValue(session);
-            if (index < 0 || index > mHotwordDetectorSessions.size() - 1) {
+            final int index = mDetectorSessions.indexOfValue(session);
+            if (index < 0 || index > mDetectorSessions.size() - 1) {
                 return;
             }
-            mHotwordDetectorSessions.removeAt(index);
+            mDetectorSessions.removeAt(index);
         }
     }
 
     @SuppressWarnings("GuardedBy")
-    private HotwordDetectorSession getDetectorSessionByTokenLocked(IBinder token) {
+    private DetectorSession getDetectorSessionByTokenLocked(IBinder token) {
         if (token == null) {
             return null;
         }
-        for (int i = 0; i < mHotwordDetectorSessions.size(); i++) {
-            final HotwordDetectorSession session = mHotwordDetectorSessions.valueAt(i);
+        for (int i = 0; i < mDetectorSessions.size(); i++) {
+            final DetectorSession session = mDetectorSessions.valueAt(i);
             if (!session.isDestroyed() && session.isSameToken(token)) {
                 return session;
             }
@@ -648,7 +651,7 @@
 
     @SuppressWarnings("GuardedBy")
     private DspTrustedHotwordDetectorSession getDspTrustedHotwordDetectorSessionLocked() {
-        final HotwordDetectorSession session = mHotwordDetectorSessions.get(
+        final DetectorSession session = mDetectorSessions.get(
                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP);
         if (session == null || session.isDestroyed()) {
             Slog.v(TAG, "Not found the Dsp detector");
@@ -659,7 +662,7 @@
 
     @SuppressWarnings("GuardedBy")
     private SoftwareTrustedHotwordDetectorSession getSoftwareTrustedHotwordDetectorSessionLocked() {
-        final HotwordDetectorSession session = mHotwordDetectorSessions.get(
+        final DetectorSession session = mDetectorSessions.get(
                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE);
         if (session == null || session.isDestroyed()) {
             Slog.v(TAG, "Not found the software detector");
@@ -669,10 +672,10 @@
     }
 
     @SuppressWarnings("GuardedBy")
-    private void runForEachHotwordDetectorSessionLocked(
-            @NonNull Consumer<HotwordDetectorSession> action) {
-        for (int i = 0; i < mHotwordDetectorSessions.size(); i++) {
-            HotwordDetectorSession session = mHotwordDetectorSessions.valueAt(i);
+    private void runForEachDetectorSessionLocked(
+            @NonNull Consumer<DetectorSession> action) {
+        for (int i = 0; i < mDetectorSessions.size(); i++) {
+            DetectorSession session = mDetectorSessions.valueAt(i);
             action.accept(session);
         }
     }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
index 4eb997a..3ad963d 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
@@ -55,7 +55,7 @@
  * {@link android.service.voice.VoiceInteractionService#createHotwordDetector(PersistableBundle,
  * SharedMemory, HotwordDetector.Callback)}.
  */
-final class SoftwareTrustedHotwordDetectorSession extends HotwordDetectorSession {
+final class SoftwareTrustedHotwordDetectorSession extends DetectorSession {
     private static final String TAG = "SoftwareTrustedHotwordDetectorSession";
 
     private IMicrophoneHotwordDetectionVoiceInteractionCallback mSoftwareCallback;
@@ -155,7 +155,7 @@
             }
         };
 
-        mRemoteHotwordDetectionService.run(
+        mRemoteDetectionService.run(
                 service -> service.detectFromMicrophoneSource(
                         null,
                         AUDIO_SOURCE_MICROPHONE,
@@ -179,7 +179,7 @@
         }
         mPerformingSoftwareHotwordDetection = false;
 
-        mRemoteHotwordDetectionService.run(ISandboxedDetectionService::stopDetection);
+        mRemoteDetectionService.run(ISandboxedDetectionService::stopDetection);
 
         closeExternalAudioStreamLocked("stopping requested");
     }
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 b064695..31babb8 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
@@ -18,6 +18,7 @@
 
 import android.app.Instrumentation
 import android.app.WallpaperManager
+import android.content.res.Resources
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
@@ -29,8 +30,8 @@
 import com.android.server.wm.flicker.helpers.NewTasksAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.DEFAULT_TASK_DISPLAY_AREA
 import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.SPLASH_SCREEN
@@ -64,9 +65,7 @@
 class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val launchNewTaskApp = NewTasksAppHelper(instrumentation)
     private val simpleApp = SimpleAppHelper(instrumentation)
-    private val wallpaper by lazy {
-        getWallpaperPackage(instrumentation) ?: error("Unable to obtain wallpaper")
-    }
+    private val wallpaper by lazy { getWallpaperPackage(instrumentation) }
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit = {
@@ -143,8 +142,7 @@
 
         val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
         flicker.assertLayers {
-            this
-                .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
+            this.invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
                     it.visibleRegion(launchNewTaskApp.componentMatcher).coversExactly(displayBounds)
                 }
                 .isInvisible(backgroundColorLayer)
@@ -159,7 +157,7 @@
                     "SIMPLE_ACTIVITY's splashscreen coversExactly displayBounds",
                     isOptional = true
                 ) {
-                    it.visibleRegion(ComponentSplashScreenMatcher( simpleApp.componentMatcher))
+                    it.visibleRegion(ComponentSplashScreenMatcher(simpleApp.componentMatcher))
                         .coversExactly(displayBounds)
                 }
                 .invoke("SIMPLE_ACTIVITY coversExactly displayBounds") {
@@ -178,7 +176,8 @@
                     isOptional = true
                 ) {
                     it.visibleRegion(
-                            ComponentSplashScreenMatcher(launchNewTaskApp.componentMatcher))
+                            ComponentSplashScreenMatcher(launchNewTaskApp.componentMatcher)
+                        )
                         .coversExactly(displayBounds)
                 }
                 .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
@@ -215,10 +214,20 @@
     override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
     companion object {
-        private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher? {
+        private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher {
             val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext)
 
             return wallpaperManager.wallpaperInfo?.component?.toFlickerComponent()
+                ?: getStaticWallpaperPackage(instrumentation)
+        }
+
+        private fun getStaticWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher {
+            val resourceId =
+                Resources.getSystem()
+                    .getIdentifier("image_wallpaper_component", "string", "android")
+            return ComponentNameMatcher.unflattenFromString(
+                instrumentation.targetContext.resources.getString(resourceId)
+            )
         }
 
         @Parameterized.Parameters(name = "{0}")
diff --git a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
index 2e93c87..3a520bc 100644
--- a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
+++ b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
@@ -37,7 +37,7 @@
 
 public class TouchLatencyActivity extends AppCompatActivity {
     private static final int REFRESH_RATE_SLIDER_MIN = 20;
-    private static final int REFRESH_RATE_SLIDER_STEP = 5;
+    private static final int REFRESH_RATE_SLIDER_STEP = 1;
 
     private Menu mMenu;
     private Mode[] mDisplayModes;
diff --git a/tests/VectorDrawableTest/OWNERS b/tests/VectorDrawableTest/OWNERS
new file mode 100644
index 0000000..27e1668
--- /dev/null
+++ b/tests/VectorDrawableTest/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 24939
+
+include /graphics/java/android/graphics/OWNERS