Merge "TvPiP: Move focus changes to pip controller" into tm-dev
diff --git a/Android.bp b/Android.bp
index f805947..933d1af 100644
--- a/Android.bp
+++ b/Android.bp
@@ -476,21 +476,28 @@
 }
 
 // TODO(b/145644363): move this to under StubLibraries.bp or ApiDocs.bp
-metalava_framework_docs_args = "--manifest $(location core/res/AndroidManifest.xml) " +
-    "--hide-package com.android.server " +
-    "--hide-package android.audio.policy.configuration.V7_0 " +
-    "--error UnhiddenSystemApi " +
-    "--hide RequiresPermission " +
-    "--hide CallbackInterface " +
-    "--hide MissingPermission --hide BroadcastBehavior " +
-    "--hide HiddenSuperclass --hide DeprecationMismatch --hide UnavailableSymbol " +
-    "--hide SdkConstant --hide HiddenTypeParameter --hide Todo --hide Typo " +
-    "--error NoSettingsProvider " +
-    "--force-convert-to-warning-nullability-annotations +*:-android.*:+android.icu.*:-dalvik.* " +
+metalava_framework_docs_args = "" +
     "--api-lint-ignore-prefix android.icu. " +
     "--api-lint-ignore-prefix java. " +
     "--api-lint-ignore-prefix junit. " +
-    "--api-lint-ignore-prefix org. "
+    "--api-lint-ignore-prefix org. " +
+    "--error NoSettingsProvider " +
+    "--error UnhiddenSystemApi " +
+    "--force-convert-to-warning-nullability-annotations +*:-android.*:+android.icu.*:-dalvik.* " +
+    "--hide BroadcastBehavior " +
+    "--hide CallbackInterface " +
+    "--hide DeprecationMismatch " +
+    "--hide HiddenSuperclass " +
+    "--hide HiddenTypeParameter " +
+    "--hide MissingPermission " +
+    "--hide-package android.audio.policy.configuration.V7_0 " +
+    "--hide-package com.android.server " +
+    "--hide RequiresPermission " +
+    "--hide SdkConstant " +
+    "--hide Todo " +
+    "--hide Typo " +
+    "--hide UnavailableSymbol " +
+    "--manifest $(location core/res/AndroidManifest.xml) "
 
 packages_to_document = [
     "android",
@@ -584,7 +591,7 @@
     libs: [
         "art.module.public.api",
         "sdk_module-lib_current_framework-tethering",
-        "sdk_module-lib_current_framework-connectivity-tiramisu",
+        "sdk_module-lib_current_framework-connectivity-t",
         "sdk_public_current_framework-bluetooth",
         // There are a few classes from modules used by the core that
         // need to be resolved by metalava. We use a prebuilt stub of the
diff --git a/StubLibraries.bp b/StubLibraries.bp
index 726ab2a..94f4374 100644
--- a/StubLibraries.bp
+++ b/StubLibraries.bp
@@ -258,7 +258,7 @@
     srcs: [":module-lib-api-stubs-docs-non-updatable"],
     libs: [
         "sdk_module-lib_current_framework-tethering",
-        "sdk_module-lib_current_framework-connectivity-tiramisu",
+        "sdk_module-lib_current_framework-connectivity-t",
         "sdk_public_current_framework-bluetooth",
         // NOTE: The below can be removed once the prebuilt stub contains bluetooth.
         "sdk_system_current_android",
diff --git a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
index 8c8d2bf..88082f7 100644
--- a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
+++ b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
@@ -39,7 +39,9 @@
     /** @hide */
     public static final String KEY_AM_MAX_SATIATED_BALANCE = "am_max_satiated_balance";
     /** @hide */
-    public static final String KEY_AM_MAX_CIRCULATION = "am_max_circulation";
+    public static final String KEY_AM_INITIAL_CONSUMPTION_LIMIT = "am_initial_consumption_limit";
+    /** @hide */
+    public static final String KEY_AM_HARD_CONSUMPTION_LIMIT = "am_hard_consumption_limit";
     // TODO: Add AlarmManager modifier keys
     /** @hide */
     public static final String KEY_AM_REWARD_TOP_ACTIVITY_INSTANT =
@@ -163,7 +165,9 @@
     public static final String KEY_JS_MAX_SATIATED_BALANCE =
             "js_max_satiated_balance";
     /** @hide */
-    public static final String KEY_JS_MAX_CIRCULATION = "js_max_circulation";
+    public static final String KEY_JS_INITIAL_CONSUMPTION_LIMIT = "js_initial_consumption_limit";
+    /** @hide */
+    public static final String KEY_JS_HARD_CONSUMPTION_LIMIT = "js_hard_consumption_limit";
     // TODO: Add JobScheduler modifier keys
     /** @hide */
     public static final String KEY_JS_REWARD_TOP_ACTIVITY_INSTANT =
@@ -280,7 +284,9 @@
     /** @hide */
     public static final int DEFAULT_AM_MAX_SATIATED_BALANCE = 1440;
     /** @hide */
-    public static final int DEFAULT_AM_MAX_CIRCULATION = 52000;
+    public static final int DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT = 28800;
+    /** @hide */
+    public static final int DEFAULT_AM_HARD_CONSUMPTION_LIMIT = 52000;
     // TODO: add AlarmManager modifier default values
     /** @hide */
     public static final int DEFAULT_AM_REWARD_TOP_ACTIVITY_INSTANT = 0;
@@ -359,7 +365,7 @@
     // Default values JobScheduler factors
     // TODO: add time_since_usage variable to min satiated balance factors
     /** @hide */
-    public static final int DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED = 50000;
+    public static final int DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED = 20000;
     /** @hide */
     public static final int DEFAULT_JS_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP = 10000;
     /** @hide */
@@ -367,7 +373,9 @@
     /** @hide */
     public static final int DEFAULT_JS_MAX_SATIATED_BALANCE = 60000;
     /** @hide */
-    public static final int DEFAULT_JS_MAX_CIRCULATION = 691200;
+    public static final int DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT = 460_000;
+    /** @hide */
+    public static final int DEFAULT_JS_HARD_CONSUMPTION_LIMIT = 900_000;
     // TODO: add JobScheduler modifier default values
     /** @hide */
     public static final int DEFAULT_JS_REWARD_TOP_ACTIVITY_INSTANT = 0;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 4e73b02..b936278 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -3007,7 +3007,7 @@
                     }
                 } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
                     if (DEBUG) {
-                        Slog.d(TAG, "Disconnected from power @ " + sElapsedRealtimeClock.millis());
+                        Slog.d(TAG, "Battery discharging @ " + sElapsedRealtimeClock.millis());
                     }
                     if (mCharging) {
                         mCharging = false;
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 a6a007f..c0a8148 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -55,9 +55,6 @@
 import com.android.server.usage.AppStandbyInternal;
 import com.android.server.utils.AlarmQueue;
 
-import libcore.util.EmptyArray;
-
-import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
@@ -105,54 +102,13 @@
     private final BalanceThresholdAlarmQueue mBalanceThresholdAlarmQueue;
 
     /**
-     * Comparator to use to sort apps before we distribute ARCs so that we try to give the most
-     * important apps ARCs first.
+     * Check the affordability notes of all apps.
      */
-    @VisibleForTesting
-    final Comparator<PackageInfo> mPackageDistributionComparator =
-            new Comparator<PackageInfo>() {
-                @Override
-                public int compare(PackageInfo pi1, PackageInfo pi2) {
-                    final ApplicationInfo appInfo1 = pi1.applicationInfo;
-                    final ApplicationInfo appInfo2 = pi2.applicationInfo;
-                    // Put any packages that don't declare an application at the end. A missing
-                    // <application> tag likely means the app won't be doing any work anyway.
-                    if (appInfo1 == null) {
-                        if (appInfo2 == null) {
-                            return 0;
-                        }
-                        return 1;
-                    } else if (appInfo2 == null) {
-                        return -1;
-                    }
-                    // Privileged apps eat first. They're likely required for the device to
-                    // function properly.
-                    // TODO: include headless system apps
-                    if (appInfo1.isPrivilegedApp()) {
-                        if (!appInfo2.isPrivilegedApp()) {
-                            return -1;
-                        }
-                    } else if (appInfo2.isPrivilegedApp()) {
-                        return 1;
-                    }
-
-                    // Sort by most recently used.
-                    final long timeSinceLastUsedMs1 =
-                            mAppStandbyInternal.getTimeSinceLastUsedByUser(
-                                    pi1.packageName, UserHandle.getUserId(pi1.applicationInfo.uid));
-                    final long timeSinceLastUsedMs2 =
-                            mAppStandbyInternal.getTimeSinceLastUsedByUser(
-                                    pi2.packageName, UserHandle.getUserId(pi2.applicationInfo.uid));
-                    if (timeSinceLastUsedMs1 < timeSinceLastUsedMs2) {
-                        return -1;
-                    } else if (timeSinceLastUsedMs1 > timeSinceLastUsedMs2) {
-                        return 1;
-                    }
-                    return 0;
-                }
-            };
-
-    private static final int MSG_CHECK_BALANCE = 0;
+    private static final int MSG_CHECK_ALL_AFFORDABILITY = 0;
+    /**
+     * Check the affordability notes of a single app.
+     */
+    private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1;
 
     Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe) {
         mLock = irs.getLock();
@@ -179,7 +135,7 @@
 
         @Override
         public void accept(OngoingEvent ongoingEvent) {
-            mTotal += getActualDeltaLocked(ongoingEvent, mLedger, mNowElapsed, mNow);
+            mTotal += getActualDeltaLocked(ongoingEvent, mLedger, mNowElapsed, mNow).price;
         }
     }
 
@@ -204,6 +160,11 @@
     }
 
     @GuardedBy("mLock")
+    private boolean isAffordableLocked(long balance, long price, long ctp) {
+        return balance >= price && mScribe.getRemainingConsumableNarcsLocked() >= ctp;
+    }
+
+    @GuardedBy("mLock")
     void noteInstantaneousEventLocked(final int userId, @NonNull final String pkgName,
             final int eventId, @Nullable String tag) {
         if (mIrs.isSystem(userId, pkgName)) {
@@ -218,10 +179,13 @@
         final int eventType = getEventType(eventId);
         switch (eventType) {
             case TYPE_ACTION:
-                final long actionCost = economicPolicy.getCostOfAction(eventId, userId, pkgName);
+                final EconomicPolicy.Cost actionCost =
+                        economicPolicy.getCostOfAction(eventId, userId, pkgName);
 
                 recordTransactionLocked(userId, pkgName, ledger,
-                        new Ledger.Transaction(now, now, eventId, tag, -actionCost), true);
+                        new Ledger.Transaction(now, now, eventId, tag,
+                                -actionCost.price, actionCost.costToProduce),
+                        true);
                 break;
 
             case TYPE_REWARD:
@@ -231,7 +195,7 @@
                     final long rewardVal = Math.max(0,
                             Math.min(reward.maxDailyReward - rewardSum, reward.instantReward));
                     recordTransactionLocked(userId, pkgName, ledger,
-                            new Ledger.Transaction(now, now, eventId, tag, rewardVal), true);
+                            new Ledger.Transaction(now, now, eventId, tag, rewardVal, 0), true);
                 }
                 break;
 
@@ -268,11 +232,12 @@
         final int eventType = getEventType(eventId);
         switch (eventType) {
             case TYPE_ACTION:
-                final long actionCost = economicPolicy.getCostOfAction(eventId, userId, pkgName);
+                final EconomicPolicy.Cost actionCost =
+                        economicPolicy.getCostOfAction(eventId, userId, pkgName);
 
                 if (ongoingEvent == null) {
                     ongoingEvents.add(eventId, tag,
-                            new OngoingEvent(eventId, tag, null, startElapsed, -actionCost));
+                            new OngoingEvent(eventId, tag, startElapsed, actionCost));
                 } else {
                     ongoingEvent.refCount++;
                 }
@@ -283,7 +248,7 @@
                 if (reward != null) {
                     if (ongoingEvent == null) {
                         ongoingEvents.add(eventId, tag, new OngoingEvent(
-                                eventId, tag, reward, startElapsed, reward.ongoingRewardPerSecond));
+                                eventId, tag, startElapsed, reward));
                     } else {
                         ongoingEvent.refCount++;
                     }
@@ -306,52 +271,7 @@
 
     @GuardedBy("mLock")
     void onPricingChangedLocked() {
-        final long now = getCurrentTimeMillis();
-        final long nowElapsed = SystemClock.elapsedRealtime();
-        final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
-
-        mCurrentOngoingEvents.forEach((userId, pkgName, ongoingEvents) -> {
-            final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
-                    mActionAffordabilityNotes.get(userId, pkgName);
-            final boolean[] wasAffordable;
-            if (actionAffordabilityNotes != null) {
-                final int size = actionAffordabilityNotes.size();
-                wasAffordable = new boolean[size];
-                for (int i = 0; i < size; ++i) {
-                    final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
-                    final long originalBalance =
-                            mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
-                    wasAffordable[i] = originalBalance >= note.getCachedModifiedPrice();
-                }
-            } else {
-                wasAffordable = EmptyArray.BOOLEAN;
-            }
-            ongoingEvents.forEach((ongoingEvent) -> {
-                // Disable balance check & affordability notifications here because we're in the
-                // middle of updating ongoing action costs/prices and sending out notifications
-                // or rescheduling the balance check alarm would be a waste since we'll have to
-                // redo them again after all of our internal state is updated.
-                stopOngoingActionLocked(userId, pkgName, ongoingEvent.eventId,
-                        ongoingEvent.tag, nowElapsed, now, false, false);
-                noteOngoingEventLocked(userId, pkgName, ongoingEvent.eventId, ongoingEvent.tag,
-                        nowElapsed, false);
-            });
-            if (actionAffordabilityNotes != null) {
-                final int size = actionAffordabilityNotes.size();
-                for (int i = 0; i < size; ++i) {
-                    final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
-                    note.recalculateModifiedPrice(economicPolicy, userId, pkgName);
-                    final long newBalance =
-                            mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
-                    final boolean isAffordable = newBalance >= note.getCachedModifiedPrice();
-                    if (wasAffordable[i] != isAffordable) {
-                        note.setNewAffordability(isAffordable);
-                        mIrs.postAffordabilityChanged(userId, pkgName, note);
-                    }
-                }
-            }
-            scheduleBalanceCheckLocked(userId, pkgName);
-        });
+        onAnythingChangedLocked(true);
     }
 
     @GuardedBy("mLock")
@@ -365,40 +285,21 @@
             SparseArrayMap<String, OngoingEvent> ongoingEvents =
                     mCurrentOngoingEvents.get(userId, pkgName);
             if (ongoingEvents != null) {
+                mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed);
+                ongoingEvents.forEach(mOngoingEventUpdater);
                 final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
                         mActionAffordabilityNotes.get(userId, pkgName);
-                final boolean[] wasAffordable;
                 if (actionAffordabilityNotes != null) {
                     final int size = actionAffordabilityNotes.size();
-                    wasAffordable = new boolean[size];
+                    final long newBalance =
+                            mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
                     for (int n = 0; n < size; ++n) {
                         final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
-                        final long originalBalance =
-                                mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
-                        wasAffordable[n] = originalBalance >= note.getCachedModifiedPrice();
-                    }
-                } else {
-                    wasAffordable = EmptyArray.BOOLEAN;
-                }
-                ongoingEvents.forEach((ongoingEvent) -> {
-                    // Disable balance check & affordability notifications here because we're in the
-                    // middle of updating ongoing action costs/prices and sending out notifications
-                    // or rescheduling the balance check alarm would be a waste since we'll have to
-                    // redo them again after all of our internal state is updated.
-                    stopOngoingActionLocked(userId, pkgName, ongoingEvent.eventId,
-                            ongoingEvent.tag, nowElapsed, now, false, false);
-                    noteOngoingEventLocked(userId, pkgName, ongoingEvent.eventId, ongoingEvent.tag,
-                            nowElapsed, false);
-                });
-                if (actionAffordabilityNotes != null) {
-                    final int size = actionAffordabilityNotes.size();
-                    for (int n = 0; n < size; ++n) {
-                        final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
-                        note.recalculateModifiedPrice(economicPolicy, userId, pkgName);
-                        final long newBalance =
-                                mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
-                        final boolean isAffordable = newBalance >= note.getCachedModifiedPrice();
-                        if (wasAffordable[n] != isAffordable) {
+                        note.recalculateCosts(economicPolicy, userId, pkgName);
+                        final boolean isAffordable =
+                                isAffordableLocked(newBalance,
+                                        note.getCachedModifiedPrice(), note.getCtp());
+                        if (note.isCurrentlyAffordable() != isAffordable) {
                             note.setNewAffordability(isAffordable);
                             mIrs.postAffordabilityChanged(userId, pkgName, note);
                         }
@@ -410,15 +311,68 @@
     }
 
     @GuardedBy("mLock")
+    private void onAnythingChangedLocked(final boolean updateOngoingEvents) {
+        final long now = getCurrentTimeMillis();
+        final long nowElapsed = SystemClock.elapsedRealtime();
+        final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
+
+        for (int uIdx = mCurrentOngoingEvents.numMaps() - 1; uIdx >= 0; --uIdx) {
+            final int userId = mCurrentOngoingEvents.keyAt(uIdx);
+
+            for (int pIdx = mCurrentOngoingEvents.numElementsForKey(userId) - 1; pIdx >= 0;
+                    --pIdx) {
+                final String pkgName = mCurrentOngoingEvents.keyAt(uIdx, pIdx);
+
+                SparseArrayMap<String, OngoingEvent> ongoingEvents =
+                        mCurrentOngoingEvents.valueAt(uIdx, pIdx);
+                if (ongoingEvents != null) {
+                    if (updateOngoingEvents) {
+                        mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed);
+                        ongoingEvents.forEach(mOngoingEventUpdater);
+                    }
+                    scheduleBalanceCheckLocked(userId, pkgName);
+                }
+            }
+        }
+        for (int uIdx = mActionAffordabilityNotes.numMaps() - 1; uIdx >= 0; --uIdx) {
+            final int userId = mActionAffordabilityNotes.keyAt(uIdx);
+
+            for (int pIdx = mActionAffordabilityNotes.numElementsForKey(userId) - 1; pIdx >= 0;
+                    --pIdx) {
+                final String pkgName = mActionAffordabilityNotes.keyAt(uIdx, pIdx);
+
+                final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
+                        mActionAffordabilityNotes.valueAt(uIdx, pIdx);
+
+                if (actionAffordabilityNotes != null) {
+                    final int size = actionAffordabilityNotes.size();
+                    final long newBalance = getBalanceLocked(userId, pkgName);
+                    for (int n = 0; n < size; ++n) {
+                        final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
+                        note.recalculateCosts(economicPolicy, userId, pkgName);
+                        final boolean isAffordable =
+                                isAffordableLocked(newBalance,
+                                        note.getCachedModifiedPrice(), note.getCtp());
+                        if (note.isCurrentlyAffordable() != isAffordable) {
+                            note.setNewAffordability(isAffordable);
+                            mIrs.postAffordabilityChanged(userId, pkgName, note);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
     void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId,
             @Nullable String tag, final long nowElapsed, final long now) {
         stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now, true, true);
     }
 
     /**
-     * @param updateBalanceCheck          Whether or not to reschedule the affordability/balance
+     * @param updateBalanceCheck          Whether to reschedule the affordability/balance
      *                                    check alarm.
-     * @param notifyOnAffordabilityChange Whether or not to evaluate the app's ability to afford
+     * @param notifyOnAffordabilityChange Whether to evaluate the app's ability to afford
      *                                    registered bills and notify listeners about any changes.
      */
     @GuardedBy("mLock")
@@ -453,9 +407,11 @@
         if (ongoingEvent.refCount <= 0) {
             final long startElapsed = ongoingEvent.startTimeElapsed;
             final long startTime = now - (nowElapsed - startElapsed);
-            final long actualDelta = getActualDeltaLocked(ongoingEvent, ledger, nowElapsed, now);
+            final EconomicPolicy.Cost actualDelta =
+                    getActualDeltaLocked(ongoingEvent, ledger, nowElapsed, now);
             recordTransactionLocked(userId, pkgName, ledger,
-                    new Ledger.Transaction(startTime, now, eventId, tag, actualDelta),
+                    new Ledger.Transaction(startTime, now, eventId, tag, actualDelta.price,
+                            actualDelta.costToProduce),
                     notifyOnAffordabilityChange);
 
             ongoingEvents.delete(eventId, tag);
@@ -466,17 +422,20 @@
     }
 
     @GuardedBy("mLock")
-    private long getActualDeltaLocked(@NonNull OngoingEvent ongoingEvent, @NonNull Ledger ledger,
-            long nowElapsed, long now) {
+    @NonNull
+    private EconomicPolicy.Cost getActualDeltaLocked(@NonNull OngoingEvent ongoingEvent,
+            @NonNull Ledger ledger, long nowElapsed, long now) {
         final long startElapsed = ongoingEvent.startTimeElapsed;
         final long durationSecs = (nowElapsed - startElapsed) / 1000;
-        final long computedDelta = durationSecs * ongoingEvent.deltaPerSec;
+        final long computedDelta = durationSecs * ongoingEvent.getDeltaPerSec();
         if (ongoingEvent.reward == null) {
-            return computedDelta;
+            return new EconomicPolicy.Cost(
+                    durationSecs * ongoingEvent.getCtpPerSec(), computedDelta);
         }
         final long rewardSum = ledger.get24HourSum(ongoingEvent.eventId, now);
-        return Math.max(0,
-                Math.min(ongoingEvent.reward.maxDailyReward - rewardSum, computedDelta));
+        return new EconomicPolicy.Cost(0,
+                Math.max(0,
+                        Math.min(ongoingEvent.reward.maxDailyReward - rewardSum, computedDelta)));
     }
 
     @VisibleForTesting
@@ -494,22 +453,6 @@
             return;
         }
         final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
-        final long maxCirculationAllowed = mIrs.getMaxCirculationLocked();
-        final long curNarcsInCirculation = mScribe.getNarcsInCirculationLocked();
-        final long newArcsInCirculation = curNarcsInCirculation + transaction.delta;
-        if (transaction.delta > 0 && newArcsInCirculation > maxCirculationAllowed) {
-            // Set lower bound at 0 so we don't accidentally take away credits when we were trying
-            // to _give_ the app credits.
-            final long newDelta = Math.max(0, maxCirculationAllowed - curNarcsInCirculation);
-            Slog.i(TAG, "Would result in too many credits in circulation. Decreasing transaction "
-                    + eventToString(transaction.eventId)
-                    + (transaction.tag == null ? "" : ":" + transaction.tag)
-                    + " for " + appToString(userId, pkgName)
-                    + " by " + narcToString(transaction.delta - newDelta));
-            transaction = new Ledger.Transaction(
-                    transaction.startTimeMs, transaction.endTimeMs,
-                    transaction.eventId, transaction.tag, newDelta);
-        }
         final long originalBalance = ledger.getCurrentBalance();
         if (transaction.delta > 0
                 && originalBalance + transaction.delta > economicPolicy.getMaxSatiatedBalance()) {
@@ -524,10 +467,10 @@
                     + " by " + narcToString(transaction.delta - newDelta));
             transaction = new Ledger.Transaction(
                     transaction.startTimeMs, transaction.endTimeMs,
-                    transaction.eventId, transaction.tag, newDelta);
+                    transaction.eventId, transaction.tag, newDelta, transaction.ctp);
         }
         ledger.recordTransaction(transaction);
-        mScribe.adjustNarcsInCirculationLocked(transaction.delta);
+        mScribe.adjustRemainingConsumableNarcsLocked(-transaction.ctp);
         if (transaction.delta != 0 && notifyOnAffordabilityChange) {
             final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
                     mActionAffordabilityNotes.get(userId, pkgName);
@@ -535,7 +478,9 @@
                 final long newBalance = ledger.getCurrentBalance();
                 for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
                     final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
-                    final boolean isAffordable = newBalance >= note.getCachedModifiedPrice();
+                    final boolean isAffordable =
+                            isAffordableLocked(newBalance,
+                                    note.getCachedModifiedPrice(), note.getCtp());
                     if (note.isCurrentlyAffordable() != isAffordable) {
                         note.setNewAffordability(isAffordable);
                         mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -543,6 +488,10 @@
                 }
             }
         }
+        if (transaction.ctp != 0) {
+            mHandler.sendEmptyMessage(MSG_CHECK_ALL_AFFORDABILITY);
+            mIrs.maybePerformQuantitativeEasingLocked();
+        }
     }
 
     /**
@@ -599,8 +548,8 @@
                         }
 
                         recordTransactionLocked(userId, pkgName, ledger,
-                                new Ledger.Transaction(
-                                        now, now, REGULATION_WEALTH_RECLAMATION, null, -toReclaim),
+                                new Ledger.Transaction(now, now, REGULATION_WEALTH_RECLAMATION,
+                                        null, -toReclaim, 0),
                                 true);
                     }
                 }
@@ -648,7 +597,7 @@
             final long now = getCurrentTimeMillis();
             final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
             recordTransactionLocked(userId, pkgName, ledger,
-                    new Ledger.Transaction(now, now, REGULATION_DEMOTION, null, -toReclaim),
+                    new Ledger.Transaction(now, now, REGULATION_DEMOTION, null, -toReclaim, 0),
                     true);
         }
     }
@@ -665,10 +614,13 @@
         return !mIrs.isSystem(userId, packageInfo.packageName);
     }
 
+    void onCreditSupplyChanged() {
+        mHandler.sendEmptyMessage(MSG_CHECK_ALL_AFFORDABILITY);
+    }
+
     @GuardedBy("mLock")
     void distributeBasicIncomeLocked(int batteryLevel) {
         List<PackageInfo> pkgs = mIrs.getInstalledPackages();
-        pkgs.sort(mPackageDistributionComparator);
 
         final long now = getCurrentTimeMillis();
         for (int i = 0; i < pkgs.size(); ++i) {
@@ -686,7 +638,7 @@
             if (shortfall > 0) {
                 recordTransactionLocked(userId, pkgName, ledger,
                         new Ledger.Transaction(now, now, REGULATION_BASIC_INCOME,
-                                null, (long) (perc * shortfall)), true);
+                                null, (long) (perc * shortfall), 0), true);
             }
         }
     }
@@ -705,12 +657,8 @@
     @GuardedBy("mLock")
     void grantBirthrightsLocked(final int userId) {
         final List<PackageInfo> pkgs = mIrs.getInstalledPackages(userId);
-        final long maxBirthright =
-                mIrs.getMaxCirculationLocked() / mIrs.getInstalledPackages().size();
         final long now = getCurrentTimeMillis();
 
-        pkgs.sort(mPackageDistributionComparator);
-
         for (int i = 0; i < pkgs.size(); ++i) {
             final PackageInfo packageInfo = pkgs.get(i);
             if (!shouldGiveCredits(packageInfo)) {
@@ -726,7 +674,7 @@
 
             recordTransactionLocked(userId, pkgName, ledger,
                     new Ledger.Transaction(now, now, REGULATION_BIRTHRIGHT, null,
-                            Math.min(maxBirthright, mIrs.getMinBalanceLocked(userId, pkgName))),
+                            mIrs.getMinBalanceLocked(userId, pkgName), 0),
                     true);
         }
     }
@@ -740,14 +688,11 @@
             return;
         }
 
-        List<PackageInfo> pkgs = mIrs.getInstalledPackages();
-        final int numPackages = pkgs.size();
-        final long maxBirthright = mIrs.getMaxCirculationLocked() / numPackages;
         final long now = getCurrentTimeMillis();
 
         recordTransactionLocked(userId, pkgName, ledger,
                 new Ledger.Transaction(now, now, REGULATION_BIRTHRIGHT, null,
-                        Math.min(maxBirthright, mIrs.getMinBalanceLocked(userId, pkgName))), true);
+                        mIrs.getMinBalanceLocked(userId, pkgName), 0), true);
     }
 
     @GuardedBy("mLock")
@@ -762,7 +707,7 @@
         final long now = getCurrentTimeMillis();
 
         recordTransactionLocked(userId, pkgName, ledger,
-                new Ledger.Transaction(now, now, REGULATION_PROMOTION, null, missing), true);
+                new Ledger.Transaction(now, now, REGULATION_PROMOTION, null, missing, 0), true);
     }
 
     @GuardedBy("mLock")
@@ -779,7 +724,7 @@
     private void reclaimAssetsLocked(final int userId, @NonNull final String pkgName) {
         final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
         if (ledger.getCurrentBalance() != 0) {
-            mScribe.adjustNarcsInCirculationLocked(-ledger.getCurrentBalance());
+            mScribe.adjustRemainingConsumableNarcsLocked(-ledger.getCurrentBalance());
         }
         mScribe.discardLedgerLocked(userId, pkgName);
         mCurrentOngoingEvents.delete(userId, pkgName);
@@ -803,6 +748,7 @@
         static final long WILL_NOT_CROSS_THRESHOLD = -1;
 
         private long mCurBalance;
+        private long mRemainingConsumableCredits;
         /**
          * The maximum change in credits per second towards the upper threshold
          * {@link #mUpperThreshold}. A value of 0 means the current ongoing events will never
@@ -815,15 +761,25 @@
          * result in the app crossing the lower threshold.
          */
         private long mMaxDeltaPerSecToLowerThreshold;
+        /**
+         * The maximum change in credits per second towards the highest CTP threshold below the
+         * remaining consumable credits (cached in {@link #mCtpThreshold}). A value of 0 means
+         * the current ongoing events will never result in the app crossing the lower threshold.
+         */
+        private long mMaxDeltaPerSecToCtpThreshold;
         private long mUpperThreshold;
         private long mLowerThreshold;
+        private long mCtpThreshold;
 
-        void reset(long curBalance,
+        void reset(long curBalance, long remainingConsumableCredits,
                 @Nullable ArraySet<ActionAffordabilityNote> actionAffordabilityNotes) {
             mCurBalance = curBalance;
+            mRemainingConsumableCredits = remainingConsumableCredits;
             mMaxDeltaPerSecToUpperThreshold = mMaxDeltaPerSecToLowerThreshold = 0;
+            mMaxDeltaPerSecToCtpThreshold = 0;
             mUpperThreshold = Long.MIN_VALUE;
             mLowerThreshold = Long.MAX_VALUE;
+            mCtpThreshold = 0;
             if (actionAffordabilityNotes != null) {
                 for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
                     final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
@@ -835,6 +791,10 @@
                         mUpperThreshold = (mUpperThreshold == Long.MIN_VALUE)
                                 ? price : Math.min(mUpperThreshold, price);
                     }
+                    final long ctp = note.getCtp();
+                    if (ctp <= mRemainingConsumableCredits) {
+                        mCtpThreshold = Math.max(mCtpThreshold, ctp);
+                    }
                 }
             }
         }
@@ -847,13 +807,23 @@
          * threshold.
          */
         long getTimeToCrossLowerThresholdMs() {
-            if (mMaxDeltaPerSecToLowerThreshold == 0) {
-                // Will never cross upper threshold based on current events.
+            if (mMaxDeltaPerSecToLowerThreshold == 0 && mMaxDeltaPerSecToCtpThreshold == 0) {
+                // Will never cross lower threshold based on current events.
                 return WILL_NOT_CROSS_THRESHOLD;
             }
-            // deltaPerSec is a negative value, so do threshold-balance to cancel out the negative.
-            final long minSeconds =
-                    (mLowerThreshold - mCurBalance) / mMaxDeltaPerSecToLowerThreshold;
+            long minSeconds = Long.MAX_VALUE;
+            if (mMaxDeltaPerSecToLowerThreshold != 0) {
+                // deltaPerSec is a negative value, so do threshold-balance to cancel out the
+                // negative.
+                minSeconds = (mLowerThreshold - mCurBalance) / mMaxDeltaPerSecToLowerThreshold;
+            }
+            if (mMaxDeltaPerSecToCtpThreshold != 0) {
+                minSeconds = Math.min(minSeconds,
+                        // deltaPerSec is a negative value, so do threshold-balance to cancel
+                        // out the negative.
+                        (mCtpThreshold - mRemainingConsumableCredits)
+                                / mMaxDeltaPerSecToCtpThreshold);
+            }
             return minSeconds * 1000;
         }
 
@@ -876,10 +846,15 @@
 
         @Override
         public void accept(OngoingEvent ongoingEvent) {
-            if (mCurBalance >= mLowerThreshold && ongoingEvent.deltaPerSec < 0) {
-                mMaxDeltaPerSecToLowerThreshold += ongoingEvent.deltaPerSec;
-            } else if (mCurBalance < mUpperThreshold && ongoingEvent.deltaPerSec > 0) {
-                mMaxDeltaPerSecToUpperThreshold += ongoingEvent.deltaPerSec;
+            final long deltaPerSec = ongoingEvent.getDeltaPerSec();
+            if (mCurBalance >= mLowerThreshold && deltaPerSec < 0) {
+                mMaxDeltaPerSecToLowerThreshold += deltaPerSec;
+            } else if (mCurBalance < mUpperThreshold && deltaPerSec > 0) {
+                mMaxDeltaPerSecToUpperThreshold += deltaPerSec;
+            }
+            final long ctpPerSec = ongoingEvent.getCtpPerSec();
+            if (mRemainingConsumableCredits >= mCtpThreshold && deltaPerSec < 0) {
+                mMaxDeltaPerSecToCtpThreshold -= ctpPerSec;
             }
         }
     }
@@ -896,8 +871,9 @@
             mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
             return;
         }
-        mTrendCalculator.reset(
-                getBalanceLocked(userId, pkgName), mActionAffordabilityNotes.get(userId, pkgName));
+        mTrendCalculator.reset(getBalanceLocked(userId, pkgName),
+                mScribe.getRemainingConsumableNarcsLocked(),
+                mActionAffordabilityNotes.get(userId, pkgName));
         ongoingEvents.forEach(mTrendCalculator);
         final long lowerTimeMs = mTrendCalculator.getTimeToCrossLowerThresholdMs();
         final long upperTimeMs = mTrendCalculator.getTimeToCrossUpperThresholdMs();
@@ -931,20 +907,79 @@
         public final String tag;
         @Nullable
         public final EconomicPolicy.Reward reward;
-        public final long deltaPerSec;
+        @Nullable
+        public final EconomicPolicy.Cost actionCost;
         public int refCount;
 
-        OngoingEvent(int eventId, @Nullable String tag,
-                @Nullable EconomicPolicy.Reward reward, long startTimeElapsed, long deltaPerSec) {
+        OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed,
+                @NonNull EconomicPolicy.Reward reward) {
             this.startTimeElapsed = startTimeElapsed;
             this.eventId = eventId;
             this.tag = tag;
             this.reward = reward;
-            this.deltaPerSec = deltaPerSec;
+            this.actionCost = null;
             refCount = 1;
         }
+
+        OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed,
+                @NonNull EconomicPolicy.Cost actionCost) {
+            this.startTimeElapsed = startTimeElapsed;
+            this.eventId = eventId;
+            this.tag = tag;
+            this.reward = null;
+            this.actionCost = actionCost;
+            refCount = 1;
+        }
+
+        long getDeltaPerSec() {
+            if (actionCost != null) {
+                return -actionCost.price;
+            }
+            if (reward != null) {
+                return reward.ongoingRewardPerSecond;
+            }
+            Slog.wtfStack(TAG, "No action or reward in ongoing event?!??!");
+            return 0;
+        }
+
+        long getCtpPerSec() {
+            if (actionCost != null) {
+                return actionCost.costToProduce;
+            }
+            return 0;
+        }
     }
 
+    private class OngoingEventUpdater implements Consumer<OngoingEvent> {
+        private int mUserId;
+        private String mPkgName;
+        private long mNow;
+        private long mNowElapsed;
+
+        private void reset(int userId, String pkgName, long now, long nowElapsed) {
+            mUserId = userId;
+            mPkgName = pkgName;
+            mNow = now;
+            mNowElapsed = nowElapsed;
+        }
+
+        @Override
+        public void accept(OngoingEvent ongoingEvent) {
+            // Disable balance check & affordability notifications here because
+            // we're in the middle of updating ongoing action costs/prices and
+            // sending out notifications or rescheduling the balance check alarm
+            // would be a waste since we'll have to redo them again after all of
+            // our internal state is updated.
+            final boolean updateBalanceCheck = false;
+            stopOngoingActionLocked(mUserId, mPkgName, ongoingEvent.eventId, ongoingEvent.tag,
+                    mNowElapsed, mNow, updateBalanceCheck, /* notifyOnAffordabilityChange */ false);
+            noteOngoingEventLocked(mUserId, mPkgName, ongoingEvent.eventId, ongoingEvent.tag,
+                    mNowElapsed, updateBalanceCheck);
+        }
+    }
+
+    private final OngoingEventUpdater mOngoingEventUpdater = new OngoingEventUpdater();
+
     private static final class Package {
         public final String packageName;
         public final int userId;
@@ -996,7 +1031,8 @@
         protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
             for (int i = 0; i < expired.size(); ++i) {
                 Package p = expired.valueAt(i);
-                mHandler.obtainMessage(MSG_CHECK_BALANCE, p.userId, 0, p.packageName)
+                mHandler.obtainMessage(
+                        MSG_CHECK_INDIVIDUAL_AFFORDABILITY, p.userId, 0, p.packageName)
                         .sendToTarget();
             }
         }
@@ -1023,9 +1059,10 @@
                 note.setNewAffordability(true);
                 return;
             }
-            note.recalculateModifiedPrice(economicPolicy, userId, pkgName);
+            note.recalculateCosts(economicPolicy, userId, pkgName);
             note.setNewAffordability(
-                    getBalanceLocked(userId, pkgName) >= note.getCachedModifiedPrice());
+                    isAffordableLocked(getBalanceLocked(userId, pkgName),
+                            note.getCachedModifiedPrice(), note.getCtp()));
             mIrs.postAffordabilityChanged(userId, pkgName, note);
             // Update ongoing alarm
             scheduleBalanceCheckLocked(userId, pkgName);
@@ -1052,6 +1089,7 @@
     static final class ActionAffordabilityNote {
         private final EconomyManagerInternal.ActionBill mActionBill;
         private final EconomyManagerInternal.AffordabilityChangeListener mListener;
+        private long mCtp;
         private long mModifiedPrice;
         private boolean mIsAffordable;
 
@@ -1086,22 +1124,29 @@
             return mModifiedPrice;
         }
 
+        private long getCtp() {
+            return mCtp;
+        }
+
         @VisibleForTesting
-        long recalculateModifiedPrice(@NonNull EconomicPolicy economicPolicy,
+        void recalculateCosts(@NonNull EconomicPolicy economicPolicy,
                 int userId, @NonNull String pkgName) {
             long modifiedPrice = 0;
+            long ctp = 0;
             final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions =
                     mActionBill.getAnticipatedActions();
             for (int i = 0; i < anticipatedActions.size(); ++i) {
                 final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i);
 
-                final long actionCost =
+                final EconomicPolicy.Cost actionCost =
                         economicPolicy.getCostOfAction(aa.actionId, userId, pkgName);
-                modifiedPrice += actionCost * aa.numInstantaneousCalls
-                        + actionCost * (aa.ongoingDurationMs / 1000);
+                modifiedPrice += actionCost.price * aa.numInstantaneousCalls
+                        + actionCost.price * (aa.ongoingDurationMs / 1000);
+                ctp += actionCost.costToProduce * aa.numInstantaneousCalls
+                        + actionCost.costToProduce * (aa.ongoingDurationMs / 1000);
             }
             mModifiedPrice = modifiedPrice;
-            return modifiedPrice;
+            mCtp = ctp;
         }
 
         boolean isCurrentlyAffordable() {
@@ -1138,7 +1183,15 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_CHECK_BALANCE: {
+                case MSG_CHECK_ALL_AFFORDABILITY: {
+                    synchronized (mLock) {
+                        removeMessages(MSG_CHECK_ALL_AFFORDABILITY);
+                        onAnythingChangedLocked(false);
+                    }
+                }
+                break;
+
+                case MSG_CHECK_INDIVIDUAL_AFFORDABILITY: {
                     final int userId = msg.arg1;
                     final String pkgName = (String) msg.obj;
                     synchronized (mLock) {
@@ -1151,8 +1204,8 @@
                             for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
                                 final ActionAffordabilityNote note =
                                         actionAffordabilityNotes.valueAt(i);
-                                final boolean isAffordable =
-                                        newBalance >= note.getCachedModifiedPrice();
+                                final boolean isAffordable = isAffordableLocked(
+                                        newBalance, note.getCachedModifiedPrice(), note.getCtp());
                                 if (note.isCurrentlyAffordable() != isAffordable) {
                                     note.setNewAffordability(isAffordable);
                                     mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -1207,7 +1260,12 @@
                         pw.print(" runtime=");
                         TimeUtils.formatDuration(nowElapsed - ongoingEvent.startTimeElapsed, pw);
                         pw.print(" delta/sec=");
-                        pw.print(ongoingEvent.deltaPerSec);
+                        pw.print(narcToString(ongoingEvent.getDeltaPerSec()));
+                        final long ctp = ongoingEvent.getCtpPerSec();
+                        if (ctp != 0) {
+                            pw.print(" ctp/sec=");
+                            pw.print(narcToString(ongoingEvent.getCtpPerSec()));
+                        }
                         pw.print(" refCount=");
                         pw.print(ongoingEvent.refCount);
                         pw.println();
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 e1e6e47..71e00cf 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
@@ -33,7 +33,8 @@
 import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_NONWAKEUP_CTP;
 import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE;
 import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP;
-import static android.app.tare.EconomyManager.DEFAULT_AM_MAX_CIRCULATION;
+import static android.app.tare.EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT;
+import static android.app.tare.EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.DEFAULT_AM_MAX_SATIATED_BALANCE;
 import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_EXEMPTED;
 import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_OTHER_APP;
@@ -70,7 +71,8 @@
 import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_NONWAKEUP_CTP;
 import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE;
 import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP;
-import static android.app.tare.EconomyManager.KEY_AM_MAX_CIRCULATION;
+import static android.app.tare.EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT;
+import static android.app.tare.EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.KEY_AM_MAX_SATIATED_BALANCE;
 import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED;
 import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP;
@@ -143,7 +145,8 @@
     private long mMinSatiatedBalanceExempted;
     private long mMinSatiatedBalanceOther;
     private long mMaxSatiatedBalance;
-    private long mMaxSatiatedCirculation;
+    private long mInitialSatiatedConsumptionLimit;
+    private long mHardSatiatedConsumptionLimit;
 
     private final KeyValueListParser mParser = new KeyValueListParser(',');
     private final InternalResourceService mInternalResourceService;
@@ -179,8 +182,13 @@
     }
 
     @Override
-    long getMaxSatiatedCirculation() {
-        return mMaxSatiatedCirculation;
+    long getInitialSatiatedConsumptionLimit() {
+        return mInitialSatiatedConsumptionLimit;
+    }
+
+    @Override
+    long getHardSatiatedConsumptionLimit() {
+        return mHardSatiatedConsumptionLimit;
     }
 
     @NonNull
@@ -217,8 +225,11 @@
                 DEFAULT_AM_MIN_SATIATED_BALANCE_OTHER_APP));
         mMaxSatiatedBalance = arcToNarc(mParser.getInt(KEY_AM_MAX_SATIATED_BALANCE,
                 DEFAULT_AM_MAX_SATIATED_BALANCE));
-        mMaxSatiatedCirculation = arcToNarc(mParser.getInt(KEY_AM_MAX_CIRCULATION,
-                DEFAULT_AM_MAX_CIRCULATION));
+        mInitialSatiatedConsumptionLimit = arcToNarc(mParser.getInt(
+                KEY_AM_INITIAL_CONSUMPTION_LIMIT, DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT));
+        mHardSatiatedConsumptionLimit = Math.max(mInitialSatiatedConsumptionLimit,
+                arcToNarc(mParser.getInt(
+                        KEY_AM_HARD_CONSUMPTION_LIMIT, DEFAULT_AM_HARD_CONSUMPTION_LIMIT)));
 
         final long exactAllowWhileIdleWakeupBasePrice = arcToNarc(
                 mParser.getInt(KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE,
@@ -357,7 +368,11 @@
         pw.print("Other", narcToString(mMinSatiatedBalanceOther)).println();
         pw.decreaseIndent();
         pw.print("Max satiated balance", narcToString(mMaxSatiatedBalance)).println();
-        pw.print("Max satiated circulation", narcToString(mMaxSatiatedCirculation)).println();
+        pw.print("Consumption limits: [");
+        pw.print(narcToString(mInitialSatiatedConsumptionLimit));
+        pw.print(", ");
+        pw.print(narcToString(mHardSatiatedConsumptionLimit));
+        pw.println("]");
 
         pw.println();
         pw.println("Actions:");
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
index a4e7b80..2109a85 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
@@ -34,7 +34,7 @@
     private final SparseArray<Reward> mRewards = new SparseArray<>();
     private final int[] mCostModifiers;
     private long mMaxSatiatedBalance;
-    private long mMaxSatiatedCirculation;
+    private long mConsumptionLimit;
 
     CompleteEconomicPolicy(@NonNull InternalResourceService irs) {
         super(irs);
@@ -74,9 +74,9 @@
 
         max = 0;
         for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) {
-            max += mEnabledEconomicPolicies.valueAt(i).getMaxSatiatedCirculation();
+            max += mEnabledEconomicPolicies.valueAt(i).getInitialSatiatedConsumptionLimit();
         }
-        mMaxSatiatedCirculation = max;
+        mConsumptionLimit = max;
     }
 
     @Override
@@ -94,8 +94,13 @@
     }
 
     @Override
-     long getMaxSatiatedCirculation() {
-        return mMaxSatiatedCirculation;
+    long getInitialSatiatedConsumptionLimit() {
+        return mConsumptionLimit;
+    }
+
+    @Override
+    long getHardSatiatedConsumptionLimit() {
+        return mConsumptionLimit;
     }
 
     @NonNull
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 c1177b2..1e48015 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
@@ -151,6 +151,16 @@
         }
     }
 
+    static class Cost {
+        public final long costToProduce;
+        public final long price;
+
+        Cost(long costToProduce, long price) {
+            this.costToProduce = costToProduce;
+            this.price = price;
+        }
+    }
+
     private static final Modifier[] COST_MODIFIER_BY_INDEX = new Modifier[NUM_COST_MODIFIERS];
 
     EconomicPolicy(@NonNull InternalResourceService irs) {
@@ -193,10 +203,18 @@
     abstract long getMaxSatiatedBalance();
 
     /**
-     * Returns the maximum number of narcs that should be in circulation at once when the device is
-     * at 100% battery.
+     * Returns the maximum number of narcs that should be consumed during a full 100% discharge
+     * cycle. This is the initial limit. The system may choose to increase the limit over time,
+     * but the increased limit should never exceed the value returned from
+     * {@link #getHardSatiatedConsumptionLimit()}.
      */
-    abstract long getMaxSatiatedCirculation();
+    abstract long getInitialSatiatedConsumptionLimit();
+
+    /**
+     * Returns the maximum number of narcs that should be consumed during a full 100% discharge
+     * cycle. This is the hard limit that should never be exceeded.
+     */
+    abstract long getHardSatiatedConsumptionLimit();
 
     /** Return the set of modifiers that should apply to this policy's costs. */
     @NonNull
@@ -211,10 +229,11 @@
     void dump(IndentingPrintWriter pw) {
     }
 
-    final long getCostOfAction(int actionId, int userId, @NonNull String pkgName) {
+    @NonNull
+    final Cost getCostOfAction(int actionId, int userId, @NonNull String pkgName) {
         final Action action = getAction(actionId);
         if (action == null) {
-            return 0;
+            return new Cost(0, 0);
         }
         long ctp = action.costToProduce;
         long price = action.basePrice;
@@ -235,7 +254,7 @@
                     (ProcessStateModifier) getModifier(COST_MODIFIER_PROCESS_STATE);
             price = processStateModifier.getModifiedPrice(userId, pkgName, ctp, price);
         }
-        return price;
+        return new Cost(ctp, price);
     }
 
     private static void initModifier(@Modifier.CostModifier final int modifierId,
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index 36895a5..c934807 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -69,6 +69,7 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.tare.EconomicPolicy.Cost;
 import com.android.server.tare.EconomyManagerInternal.TareStateChangeListener;
 
 import java.io.FileDescriptor;
@@ -100,6 +101,11 @@
     private static final long MIN_UNUSED_TIME_MS = 3 * DAY_IN_MILLIS;
     /** The amount of time to delay reclamation by after boot. */
     private static final long RECLAMATION_STARTUP_DELAY_MS = 30_000L;
+    /**
+     * The battery level above which we may consider quantitative easing (increasing the consumption
+     * limit).
+     */
+    private static final int QUANTITATIVE_EASING_BATTERY_THRESHOLD = 50;
     private static final int PACKAGE_QUERY_FLAGS =
             PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                     | PackageManager.MATCH_APEX;
@@ -122,44 +128,6 @@
     @GuardedBy("mLock")
     private CompleteEconomicPolicy mCompleteEconomicPolicy;
 
-    private static final class ReclamationConfig {
-        /**
-         * ARC circulation threshold (% circulating vs scaled maximum) above which this config
-         * should come into play.
-         */
-        public final double circulationPercentageThreshold;
-        /** @see Agent#reclaimUnusedAssetsLocked(double, long, boolean) */
-        public final double reclamationPercentage;
-        /** @see Agent#reclaimUnusedAssetsLocked(double, long, boolean) */
-        public final long minUsedTimeMs;
-        /** @see Agent#reclaimUnusedAssetsLocked(double, long, boolean) */
-        public final boolean scaleMinBalance;
-
-        ReclamationConfig(double circulationPercentageThreshold, double reclamationPercentage,
-                long minUsedTimeMs, boolean scaleMinBalance) {
-            this.circulationPercentageThreshold = circulationPercentageThreshold;
-            this.reclamationPercentage = reclamationPercentage;
-            this.minUsedTimeMs = minUsedTimeMs;
-            this.scaleMinBalance = scaleMinBalance;
-        }
-    }
-
-    /**
-     * Sorted list of reclamation configs used to determine how many credits to force reclaim when
-     * the circulation percentage is too high. The list should *always* be sorted in descending
-     * order of {@link ReclamationConfig#circulationPercentageThreshold}.
-     */
-    @GuardedBy("mLock")
-    private final List<ReclamationConfig> mReclamationConfigs = List.of(
-            new ReclamationConfig(2, .75, 12 * HOUR_IN_MILLIS, true),
-            new ReclamationConfig(1.6, .5, DAY_IN_MILLIS, true),
-            new ReclamationConfig(1.4, .25, DAY_IN_MILLIS, true),
-            new ReclamationConfig(1.2, .25, 2 * DAY_IN_MILLIS, true),
-            new ReclamationConfig(1, .25, MIN_UNUSED_TIME_MS, false),
-            new ReclamationConfig(
-                    .9, DEFAULT_UNUSED_RECLAMATION_PERCENTAGE, MIN_UNUSED_TIME_MS, false)
-    );
-
     @NonNull
     @GuardedBy("mLock")
     private final List<PackageInfo> mPkgCache = new ArrayList<>();
@@ -266,8 +234,7 @@
     private static final int MSG_NOTIFY_AFFORDABILITY_CHANGE_LISTENER = 0;
     private static final int MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT = 1;
     private static final int MSG_PROCESS_USAGE_EVENT = 2;
-    private static final int MSG_MAYBE_FORCE_RECLAIM = 3;
-    private static final int MSG_NOTIFY_STATE_CHANGE_LISTENERS = 4;
+    private static final int MSG_NOTIFY_STATE_CHANGE_LISTENERS = 3;
     private static final String ALARM_TAG_WEALTH_RECLAMATION = "*tare.reclamation*";
 
     /**
@@ -362,8 +329,8 @@
     }
 
     @GuardedBy("mLock")
-    long getMaxCirculationLocked() {
-        return mCurrentBatteryLevel * mCompleteEconomicPolicy.getMaxSatiatedCirculation() / 100;
+    long getConsumptionLimitLocked() {
+        return mCurrentBatteryLevel * mScribe.getSatiatedConsumptionLimitLocked() / 100;
     }
 
     @GuardedBy("mLock")
@@ -372,6 +339,11 @@
                 / 100;
     }
 
+    @GuardedBy("mLock")
+    long getInitialSatiatedConsumptionLimitLocked() {
+        return mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit();
+    }
+
     int getUid(final int userId, @NonNull final String pkgName) {
         synchronized (mPackageToUidCache) {
             Integer uid = mPackageToUidCache.get(userId, pkgName);
@@ -403,12 +375,15 @@
     void onBatteryLevelChanged() {
         synchronized (mLock) {
             final int newBatteryLevel = getCurrentBatteryLevel();
-            if (newBatteryLevel > mCurrentBatteryLevel) {
+            final boolean increased = newBatteryLevel > mCurrentBatteryLevel;
+            if (increased) {
                 mAgent.distributeBasicIncomeLocked(newBatteryLevel);
-            } else if (newBatteryLevel < mCurrentBatteryLevel) {
-                mHandler.obtainMessage(MSG_MAYBE_FORCE_RECLAIM).sendToTarget();
+            } else if (newBatteryLevel == mCurrentBatteryLevel) {
+                Slog.wtf(TAG, "Battery level stayed the same");
+                return;
             }
             mCurrentBatteryLevel = newBatteryLevel;
+            adjustCreditSupplyLocked(increased);
         }
     }
 
@@ -546,6 +521,31 @@
         }
     }
 
+    /**
+     * Try to increase the consumption limit if apps are reaching the current limit too quickly.
+     */
+    @GuardedBy("mLock")
+    void maybePerformQuantitativeEasingLocked() {
+        // We don't need to increase the limit if the device runs out of consumable credits
+        // when the battery is low.
+        final long remainingConsumableNarcs = mScribe.getRemainingConsumableNarcsLocked();
+        if (mCurrentBatteryLevel <= QUANTITATIVE_EASING_BATTERY_THRESHOLD
+                || remainingConsumableNarcs > 0) {
+            return;
+        }
+        final long currentConsumptionLimit = mScribe.getSatiatedConsumptionLimitLocked();
+        final long shortfall = (mCurrentBatteryLevel - QUANTITATIVE_EASING_BATTERY_THRESHOLD)
+                * currentConsumptionLimit / 100;
+        final long newConsumptionLimit = Math.min(currentConsumptionLimit + shortfall,
+                mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit());
+        if (newConsumptionLimit != currentConsumptionLimit) {
+            Slog.i(TAG, "Increasing consumption limit from " + narcToString(currentConsumptionLimit)
+                    + " to " + narcToString(newConsumptionLimit));
+            mScribe.setConsumptionLimitLocked(newConsumptionLimit);
+            adjustCreditSupplyLocked(/* allowIncrease */ true);
+        }
+    }
+
     void postAffordabilityChanged(final int userId, @NonNull final String pkgName,
             @NonNull Agent.ActionAffordabilityNote affordabilityNote) {
         if (DEBUG) {
@@ -560,6 +560,23 @@
     }
 
     @GuardedBy("mLock")
+    private void adjustCreditSupplyLocked(boolean allowIncrease) {
+        final long newLimit = getConsumptionLimitLocked();
+        final long remainingConsumableNarcs = mScribe.getRemainingConsumableNarcsLocked();
+        if (remainingConsumableNarcs == newLimit) {
+            return;
+        }
+        if (remainingConsumableNarcs > newLimit) {
+            mScribe.adjustRemainingConsumableNarcsLocked(newLimit - remainingConsumableNarcs);
+        } else if (allowIncrease) {
+            final double perc = mCurrentBatteryLevel / 100d;
+            final long shortfall = newLimit - remainingConsumableNarcs;
+            mScribe.adjustRemainingConsumableNarcsLocked((long) (perc * shortfall));
+        }
+        mAgent.onCreditSupplyChanged();
+    }
+
+    @GuardedBy("mLock")
     private void processUsageEventLocked(final int userId, @NonNull UsageEvents.Event event) {
         if (!mIsEnabled) {
             return;
@@ -650,48 +667,6 @@
         }
     }
 
-    /**
-     * Reclaim unused ARCs above apps' minimum balances if there are too many credits currently
-     * in circulation.
-     */
-    @GuardedBy("mLock")
-    private void maybeForceReclaimLocked() {
-        final long maxCirculation = getMaxCirculationLocked();
-        if (maxCirculation == 0) {
-            Slog.wtf(TAG, "Max scaled circulation is 0...");
-            mAgent.reclaimUnusedAssetsLocked(1, HOUR_IN_MILLIS, true);
-            mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis());
-            scheduleUnusedWealthReclamationLocked();
-            return;
-        }
-        final long curCirculation = mScribe.getNarcsInCirculationLocked();
-        final double circulationPerc = 1.0 * curCirculation / maxCirculation;
-        if (DEBUG) {
-            Slog.d(TAG, "Circulation %: " + circulationPerc);
-        }
-        final int numConfigs = mReclamationConfigs.size();
-        if (numConfigs == 0) {
-            return;
-        }
-        // The configs are sorted in descending order of circulationPercentageThreshold, so we can
-        // short-circuit if the current circulation is lower than the lowest threshold.
-        if (circulationPerc
-                < mReclamationConfigs.get(numConfigs - 1).circulationPercentageThreshold) {
-            return;
-        }
-        // TODO: maybe exclude apps we think will be launched in the next few hours
-        for (int i = 0; i < numConfigs; ++i) {
-            final ReclamationConfig config = mReclamationConfigs.get(i);
-            if (circulationPerc >= config.circulationPercentageThreshold) {
-                mAgent.reclaimUnusedAssetsLocked(
-                        config.reclamationPercentage, config.minUsedTimeMs, config.scaleMinBalance);
-                mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis());
-                scheduleUnusedWealthReclamationLocked();
-                break;
-            }
-        }
-    }
-
     private void registerListeners() {
         final IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_BATTERY_LEVEL_CHANGED);
@@ -731,8 +706,18 @@
             final boolean isFirstSetup = !mScribe.recordExists();
             if (isFirstSetup) {
                 mAgent.grantBirthrightsLocked();
+                mScribe.setConsumptionLimitLocked(
+                        mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
             } else {
                 mScribe.loadFromDiskLocked();
+                if (mScribe.getSatiatedConsumptionLimitLocked()
+                        < mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()
+                        || mScribe.getSatiatedConsumptionLimitLocked()
+                        > mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()) {
+                    // Reset the consumption limit since several factors may have changed.
+                    mScribe.setConsumptionLimitLocked(
+                            mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
+                }
             }
             scheduleUnusedWealthReclamationLocked();
         }
@@ -787,14 +772,6 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_MAYBE_FORCE_RECLAIM: {
-                    removeMessages(MSG_MAYBE_FORCE_RECLAIM);
-                    synchronized (mLock) {
-                        maybeForceReclaimLocked();
-                    }
-                }
-                break;
-
                 case MSG_NOTIFY_AFFORDABILITY_CHANGE_LISTENER: {
                     final SomeArgs args = (SomeArgs) msg.obj;
                     final int userId = args.argi1;
@@ -936,12 +913,13 @@
             synchronized (mLock) {
                 for (int i = 0; i < projectedActions.size(); ++i) {
                     AnticipatedAction action = projectedActions.get(i);
-                    final long cost = mCompleteEconomicPolicy.getCostOfAction(
+                    final Cost cost = mCompleteEconomicPolicy.getCostOfAction(
                             action.actionId, userId, pkgName);
-                    requiredBalance += cost * action.numInstantaneousCalls
-                            + cost * (action.ongoingDurationMs / 1000);
+                    requiredBalance += cost.price * action.numInstantaneousCalls
+                            + cost.price * (action.ongoingDurationMs / 1000);
                 }
-                return mAgent.getBalanceLocked(userId, pkgName) >= requiredBalance;
+                return mAgent.getBalanceLocked(userId, pkgName) >= requiredBalance
+                        && mScribe.getRemainingConsumableNarcsLocked() >= requiredBalance;
             }
         }
 
@@ -960,14 +938,17 @@
             synchronized (mLock) {
                 for (int i = 0; i < projectedActions.size(); ++i) {
                     AnticipatedAction action = projectedActions.get(i);
-                    final long cost = mCompleteEconomicPolicy.getCostOfAction(
+                    final Cost cost = mCompleteEconomicPolicy.getCostOfAction(
                             action.actionId, userId, pkgName);
-                    totalCostPerSecond += cost;
+                    totalCostPerSecond += cost.price;
                 }
                 if (totalCostPerSecond == 0) {
                     return FOREVER_MS;
                 }
-                return mAgent.getBalanceLocked(userId, pkgName) * 1000 / totalCostPerSecond;
+                final long minBalance = Math.min(
+                        mAgent.getBalanceLocked(userId, pkgName),
+                        mScribe.getRemainingConsumableNarcsLocked());
+                return minBalance * 1000 / totalCostPerSecond;
             }
         }
 
@@ -1085,10 +1066,20 @@
 
         private void updateEconomicPolicy() {
             synchronized (mLock) {
+                final long initialLimit =
+                        mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit();
+                final long hardLimit = mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit();
                 mCompleteEconomicPolicy.tearDown();
                 mCompleteEconomicPolicy = new CompleteEconomicPolicy(InternalResourceService.this);
                 if (mIsEnabled && mBootPhase >= PHASE_SYSTEM_SERVICES_READY) {
                     mCompleteEconomicPolicy.setup();
+                    if (initialLimit != mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()
+                            || hardLimit
+                            != mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()) {
+                        // Reset the consumption limit since several factors may have changed.
+                        mScribe.setConsumptionLimitLocked(
+                                mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
+                    }
                     mAgent.onPricingChangedLocked();
                 }
             }
@@ -1110,18 +1101,23 @@
             pw.print("Current battery level: ");
             pw.println(mCurrentBatteryLevel);
 
-            final long maxCirculation = getMaxCirculationLocked();
-            pw.print("Max circulation (current/satiated): ");
-            pw.print(narcToString(maxCirculation));
+            final long consumptionLimit = getConsumptionLimitLocked();
+            pw.print("Consumption limit (current/initial-satiated/current-satiated): ");
+            pw.print(narcToString(consumptionLimit));
             pw.print("/");
-            pw.println(narcToString(mCompleteEconomicPolicy.getMaxSatiatedCirculation()));
+            pw.print(narcToString(mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()));
+            pw.print("/");
+            pw.println(narcToString(mScribe.getSatiatedConsumptionLimitLocked()));
 
-            final long currentCirculation = mScribe.getNarcsInCirculationLocked();
-            pw.print("Current GDP: ");
-            pw.print(narcToString(currentCirculation));
+            final long remainingConsumable = mScribe.getRemainingConsumableNarcsLocked();
+            pw.print("Goods remaining: ");
+            pw.print(narcToString(remainingConsumable));
             pw.print(" (");
-            pw.print(String.format("%.2f", 100f * currentCirculation / maxCirculation));
-            pw.println("% of current max)");
+            pw.print(String.format("%.2f", 100f * remainingConsumable / consumptionLimit));
+            pw.println("% of current limit)");
+
+            pw.print("Device wealth: ");
+            pw.println(narcToString(mScribe.getNarcsInCirculationForLoggingLocked()));
 
             pw.println();
             pw.print("Exempted apps", mExemptedApps);
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
index 1f8ce26..0eddd22 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
@@ -38,7 +38,8 @@
 import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_MIN_START_CTP;
 import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE;
 import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_CTP;
-import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_CIRCULATION;
+import static android.app.tare.EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT;
+import static android.app.tare.EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE;
 import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED;
 import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP;
@@ -79,7 +80,8 @@
 import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_MIN_START_CTP;
 import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE;
 import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_TIMEOUT_PENALTY_CTP;
-import static android.app.tare.EconomyManager.KEY_JS_MAX_CIRCULATION;
+import static android.app.tare.EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT;
+import static android.app.tare.EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.KEY_JS_MAX_SATIATED_BALANCE;
 import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED;
 import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP;
@@ -145,7 +147,8 @@
     private long mMinSatiatedBalanceExempted;
     private long mMinSatiatedBalanceOther;
     private long mMaxSatiatedBalance;
-    private long mMaxSatiatedCirculation;
+    private long mInitialSatiatedConsumptionLimit;
+    private long mHardSatiatedConsumptionLimit;
 
     private final KeyValueListParser mParser = new KeyValueListParser(',');
     private final InternalResourceService mInternalResourceService;
@@ -181,8 +184,13 @@
     }
 
     @Override
-    long getMaxSatiatedCirculation() {
-        return mMaxSatiatedCirculation;
+    long getInitialSatiatedConsumptionLimit() {
+        return mInitialSatiatedConsumptionLimit;
+    }
+
+    @Override
+    long getHardSatiatedConsumptionLimit() {
+        return mHardSatiatedConsumptionLimit;
     }
 
     @NonNull
@@ -221,8 +229,11 @@
                         DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP));
         mMaxSatiatedBalance = arcToNarc(mParser.getInt(KEY_JS_MAX_SATIATED_BALANCE,
                 DEFAULT_JS_MAX_SATIATED_BALANCE));
-        mMaxSatiatedCirculation = arcToNarc(mParser.getInt(KEY_JS_MAX_CIRCULATION,
-                DEFAULT_JS_MAX_CIRCULATION));
+        mInitialSatiatedConsumptionLimit = arcToNarc(mParser.getInt(
+                KEY_JS_INITIAL_CONSUMPTION_LIMIT, DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT));
+        mHardSatiatedConsumptionLimit = Math.max(mInitialSatiatedConsumptionLimit,
+                arcToNarc(mParser.getInt(
+                        KEY_JS_HARD_CONSUMPTION_LIMIT, DEFAULT_JS_HARD_CONSUMPTION_LIMIT)));
 
         mActions.put(ACTION_JOB_MAX_START, new Action(ACTION_JOB_MAX_START,
                 arcToNarc(mParser.getInt(KEY_JS_ACTION_JOB_MAX_START_CTP,
@@ -332,7 +343,11 @@
         pw.print("Other", narcToString(mMinSatiatedBalanceOther)).println();
         pw.decreaseIndent();
         pw.print("Max satiated balance", narcToString(mMaxSatiatedBalance)).println();
-        pw.print("Max satiated circulation", narcToString(mMaxSatiatedCirculation)).println();
+        pw.print("Consumption limits: [");
+        pw.print(narcToString(mInitialSatiatedConsumptionLimit));
+        pw.print(", ");
+        pw.print(narcToString(mHardSatiatedConsumptionLimit));
+        pw.println("]");
 
         pw.println();
         pw.println("Actions:");
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
index f4917ad..dfdc20a 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
@@ -41,14 +41,16 @@
         @Nullable
         public final String tag;
         public final long delta;
+        public final long ctp;
 
         Transaction(long startTimeMs, long endTimeMs,
-                int eventId, @Nullable String tag, long delta) {
+                int eventId, @Nullable String tag, long delta, long ctp) {
             this.startTimeMs = startTimeMs;
             this.endTimeMs = endTimeMs;
             this.eventId = eventId;
             this.tag = tag;
             this.delta = delta;
+            this.ctp = ctp;
         }
     }
 
@@ -144,7 +146,10 @@
                 pw.print(")");
             }
             pw.print(" --> ");
-            pw.println(narcToString(transaction.delta));
+            pw.print(narcToString(transaction.delta));
+            pw.print(" (ctp=");
+            pw.print(narcToString(transaction.ctp));
+            pw.println(")");
         }
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
index 86968ef..8662110 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -73,6 +73,7 @@
     private static final String XML_TAG_TRANSACTION = "transaction";
     private static final String XML_TAG_USER = "user";
 
+    private static final String XML_ATTR_CTP = "ctp";
     private static final String XML_ATTR_DELTA = "delta";
     private static final String XML_ATTR_EVENT_ID = "eventId";
     private static final String XML_ATTR_TAG = "tag";
@@ -83,6 +84,8 @@
     private static final String XML_ATTR_USER_ID = "userId";
     private static final String XML_ATTR_VERSION = "version";
     private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
+    private static final String XML_ATTR_REMAINING_CONSUMABLE_NARCS = "remainingConsumableNarcs";
+    private static final String XML_ATTR_CONSUMPTION_LIMIT = "consumptionLimit";
 
     /** Version of the file schema. */
     private static final int STATE_FILE_VERSION = 0;
@@ -95,7 +98,9 @@
     @GuardedBy("mIrs.getLock()")
     private long mLastReclamationTime;
     @GuardedBy("mIrs.getLock()")
-    private long mNarcsInCirculation;
+    private long mSatiatedConsumptionLimit;
+    @GuardedBy("mIrs.getLock()")
+    private long mRemainingConsumableNarcs;
     @GuardedBy("mIrs.getLock()")
     private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();
 
@@ -117,10 +122,10 @@
     }
 
     @GuardedBy("mIrs.getLock()")
-    void adjustNarcsInCirculationLocked(long delta) {
+    void adjustRemainingConsumableNarcsLocked(long delta) {
         if (delta != 0) {
             // No point doing any work if the change is 0.
-            mNarcsInCirculation += delta;
+            mRemainingConsumableNarcs += delta;
             postWrite();
         }
     }
@@ -132,6 +137,11 @@
     }
 
     @GuardedBy("mIrs.getLock()")
+    long getSatiatedConsumptionLimitLocked() {
+        return mSatiatedConsumptionLimit;
+    }
+
+    @GuardedBy("mIrs.getLock()")
     long getLastReclamationTimeLocked() {
         return mLastReclamationTime;
     }
@@ -153,19 +163,37 @@
         return mLedgers;
     }
 
-    /** Returns the total amount of narcs currently allocated to apps. */
+    /**
+     * Returns the sum of credits granted to all apps on the system. This is expensive so don't
+     * call it for normal operation.
+     */
     @GuardedBy("mIrs.getLock()")
-    long getNarcsInCirculationLocked() {
-        return mNarcsInCirculation;
+    long getNarcsInCirculationForLoggingLocked() {
+        long sum = 0;
+        for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
+            for (int pIdx = mLedgers.numElementsForKeyAt(uIdx) - 1; pIdx >= 0; --pIdx) {
+                sum += mLedgers.valueAt(uIdx, pIdx).getCurrentBalance();
+            }
+        }
+        return sum;
+    }
+
+    /** Returns the total amount of narcs that remain to be consumed. */
+    @GuardedBy("mIrs.getLock()")
+    long getRemainingConsumableNarcsLocked() {
+        return mRemainingConsumableNarcs;
     }
 
     @GuardedBy("mIrs.getLock()")
     void loadFromDiskLocked() {
         mLedgers.clear();
-        mNarcsInCirculation = 0;
         if (!recordExists()) {
+            mSatiatedConsumptionLimit = mIrs.getInitialSatiatedConsumptionLimitLocked();
+            mRemainingConsumableNarcs = mIrs.getConsumptionLimitLocked();
             return;
         }
+        mSatiatedConsumptionLimit = 0;
+        mRemainingConsumableNarcs = 0;
 
         final SparseArray<ArraySet<String>> installedPackagesPerUser = new SparseArray<>();
         final List<PackageInfo> installedPackages = mIrs.getInstalledPackages();
@@ -222,6 +250,13 @@
                     case XML_TAG_HIGH_LEVEL_STATE:
                         mLastReclamationTime =
                                 parser.getAttributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME);
+                        mSatiatedConsumptionLimit =
+                                parser.getAttributeLong(null, XML_ATTR_CONSUMPTION_LIMIT,
+                                        mIrs.getInitialSatiatedConsumptionLimitLocked());
+                        final long consumptionLimit = mIrs.getConsumptionLimitLocked();
+                        mRemainingConsumableNarcs = Math.min(consumptionLimit,
+                                parser.getAttributeLong(null, XML_ATTR_REMAINING_CONSUMABLE_NARCS,
+                                        consumptionLimit));
                         break;
                     case XML_TAG_USER:
                         earliestEndTime = Math.min(earliestEndTime,
@@ -249,6 +284,18 @@
     }
 
     @GuardedBy("mIrs.getLock()")
+    void setConsumptionLimitLocked(long limit) {
+        if (mRemainingConsumableNarcs > limit) {
+            mRemainingConsumableNarcs = limit;
+        } else if (limit > mSatiatedConsumptionLimit) {
+            final long diff = mSatiatedConsumptionLimit - mRemainingConsumableNarcs;
+            mRemainingConsumableNarcs = (limit - diff);
+        }
+        mSatiatedConsumptionLimit = limit;
+        postWrite();
+    }
+
+    @GuardedBy("mIrs.getLock()")
     void setLastReclamationTimeLocked(long time) {
         mLastReclamationTime = time;
         postWrite();
@@ -259,7 +306,8 @@
         TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
         TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
         mLedgers.clear();
-        mNarcsInCirculation = 0;
+        mRemainingConsumableNarcs = 0;
+        mSatiatedConsumptionLimit = 0;
         mLastReclamationTime = 0;
     }
 
@@ -339,13 +387,14 @@
             final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
             final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
             final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
+            final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
             if (endTime <= endTimeCutoff) {
                 if (DEBUG) {
                     Slog.d(TAG, "Skipping event because it's too old.");
                 }
                 continue;
             }
-            transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta));
+            transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
         }
 
         if (!isInstalled) {
@@ -395,7 +444,6 @@
                 final Ledger ledger = ledgerData.second;
                 if (ledger != null) {
                     mLedgers.add(curUser, ledgerData.first, ledger);
-                    mNarcsInCirculation += Math.max(0, ledger.getCurrentBalance());
                     final Ledger.Transaction transaction = ledger.getEarliestTransaction();
                     if (transaction != null) {
                         earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
@@ -442,6 +490,9 @@
 
                 out.startTag(null, XML_TAG_HIGH_LEVEL_STATE);
                 out.attributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME, mLastReclamationTime);
+                out.attributeLong(null, XML_ATTR_CONSUMPTION_LIMIT, mSatiatedConsumptionLimit);
+                out.attributeLong(null, XML_ATTR_REMAINING_CONSUMABLE_NARCS,
+                        mRemainingConsumableNarcs);
                 out.endTag(null, XML_TAG_HIGH_LEVEL_STATE);
 
                 for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
@@ -505,6 +556,7 @@
             out.attribute(null, XML_ATTR_TAG, transaction.tag);
         }
         out.attributeLong(null, XML_ATTR_DELTA, transaction.delta);
+        out.attributeLong(null, XML_ATTR_CTP, transaction.ctp);
         out.endTag(null, XML_TAG_TRANSACTION);
     }
 
diff --git a/api/Android.bp b/api/Android.bp
index 66c7823..bbe26b7 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -113,7 +113,7 @@
         "framework-appsearch",
         "framework-bluetooth",
         "framework-connectivity",
-        "framework-connectivity-tiramisu",
+        "framework-connectivity-t",
         "framework-graphics",
         "framework-media",
         "framework-mediaprovider",
diff --git a/core/api/current.txt b/core/api/current.txt
index 56415ec..3a6fc59 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7800,7 +7800,6 @@
   }
 
   public final class DevicePolicyResources {
-    ctor public DevicePolicyResources();
   }
 
   public static final class DevicePolicyResources.Drawables {
@@ -14391,6 +14390,8 @@
     method @NonNull @Size(min=3) public abstract float[] fromXyz(@NonNull @Size(min=3) float[]);
     method @NonNull public static android.graphics.ColorSpace get(@NonNull android.graphics.ColorSpace.Named);
     method @IntRange(from=1, to=4) public int getComponentCount();
+    method public int getDataSpace();
+    method @Nullable public static android.graphics.ColorSpace getFromDataSpace(int);
     method @IntRange(from=android.graphics.ColorSpace.MIN_ID, to=android.graphics.ColorSpace.MAX_ID) public int getId();
     method public abstract float getMaxValue(@IntRange(from=0, to=3) int);
     method public abstract float getMinValue(@IntRange(from=0, to=3) int);
@@ -27446,7 +27447,6 @@
     field public static final String CATEGORY_PAYMENT = "payment";
     field public static final String EXTRA_CATEGORY = "category";
     field public static final String EXTRA_SERVICE_COMPONENT = "component";
-    field public static final String EXTRA_USERID = "android.nfc.cardemulation.extra.USERID";
     field public static final int SELECTION_MODE_ALWAYS_ASK = 1; // 0x1
     field public static final int SELECTION_MODE_ASK_IF_CONFLICT = 2; // 0x2
     field public static final int SELECTION_MODE_PREFER_DEFAULT = 0; // 0x0
@@ -30686,7 +30686,7 @@
   public class BaseBundle {
     method public void clear();
     method public boolean containsKey(String);
-    method @Nullable public Object get(String);
+    method @Deprecated @Nullable public Object get(String);
     method public boolean getBoolean(String);
     method public boolean getBoolean(String, boolean);
     method @Nullable public boolean[] getBooleanArray(@Nullable String);
@@ -30928,16 +30928,21 @@
     method public float getFloat(String, float);
     method @Nullable public float[] getFloatArray(@Nullable String);
     method @Nullable public java.util.ArrayList<java.lang.Integer> getIntegerArrayList(@Nullable String);
-    method @Nullable public <T extends android.os.Parcelable> T getParcelable(@Nullable String);
-    method @Nullable public android.os.Parcelable[] getParcelableArray(@Nullable String);
-    method @Nullable public <T extends android.os.Parcelable> java.util.ArrayList<T> getParcelableArrayList(@Nullable String);
-    method @Nullable public java.io.Serializable getSerializable(@Nullable String);
+    method @Deprecated @Nullable public <T extends android.os.Parcelable> T getParcelable(@Nullable String);
+    method @Nullable public <T> T getParcelable(@Nullable String, @NonNull Class<T>);
+    method @Deprecated @Nullable public android.os.Parcelable[] getParcelableArray(@Nullable String);
+    method @Nullable public <T> T[] getParcelableArray(@Nullable String, @NonNull Class<T>);
+    method @Deprecated @Nullable public <T extends android.os.Parcelable> java.util.ArrayList<T> getParcelableArrayList(@Nullable String);
+    method @Nullable public <T> java.util.ArrayList<T> getParcelableArrayList(@Nullable String, @NonNull Class<T>);
+    method @Deprecated @Nullable public java.io.Serializable getSerializable(@Nullable String);
+    method @Nullable public <T extends java.io.Serializable> T getSerializable(@Nullable String, @NonNull Class<T>);
     method public short getShort(String);
     method public short getShort(String, short);
     method @Nullable public short[] getShortArray(@Nullable String);
     method @Nullable public android.util.Size getSize(@Nullable String);
     method @Nullable public android.util.SizeF getSizeF(@Nullable String);
-    method @Nullable public <T extends android.os.Parcelable> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String);
+    method @Deprecated @Nullable public <T extends android.os.Parcelable> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String);
+    method @Nullable public <T> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String, @NonNull Class<T>);
     method @Nullable public java.util.ArrayList<java.lang.String> getStringArrayList(@Nullable String);
     method public boolean hasFileDescriptors();
     method public void putAll(android.os.Bundle);
@@ -49287,7 +49292,7 @@
     method @NonNull public android.view.SurfaceControl.Transaction setDataSpace(@NonNull android.view.SurfaceControl, int);
     method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int);
     method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int, int);
-    method @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int);
+    method @Deprecated @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int);
     method @NonNull public android.view.SurfaceControl.Transaction setLayer(@NonNull android.view.SurfaceControl, @IntRange(from=java.lang.Integer.MIN_VALUE, to=java.lang.Integer.MAX_VALUE) int);
     method @NonNull public android.view.SurfaceControl.Transaction setOpaque(@NonNull android.view.SurfaceControl, boolean);
     method @NonNull public android.view.SurfaceControl.Transaction setPosition(@NonNull android.view.SurfaceControl, float, float);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 2c29343..2c0e641 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -1075,7 +1075,7 @@
   }
 
   public class DevicePolicyManager {
-    method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public int checkProvisioningPreCondition(@NonNull String, @NonNull String);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public int checkProvisioningPrecondition(@NonNull String, @NonNull String);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public android.os.UserHandle createAndProvisionManagedProfile(@NonNull android.app.admin.ManagedProfileProvisioningParams) throws android.app.admin.ProvisioningException;
     method @Nullable public android.content.Intent createProvisioningIntentFromNfcIntent(@NonNull android.content.Intent);
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public boolean getBluetoothContactSharingDisabled(@NonNull android.os.UserHandle);
@@ -1128,21 +1128,6 @@
     field public static final String ACTION_SET_PROFILE_OWNER = "android.app.action.SET_PROFILE_OWNER";
     field @Deprecated public static final String ACTION_STATE_USER_SETUP_COMPLETE = "android.app.action.STATE_USER_SETUP_COMPLETE";
     field @RequiresPermission(android.Manifest.permission.LAUNCH_DEVICE_MANAGER_SETUP) public static final String ACTION_UPDATE_DEVICE_MANAGEMENT_ROLE_HOLDER = "android.app.action.UPDATE_DEVICE_MANAGEMENT_ROLE_HOLDER";
-    field public static final int CODE_ACCOUNTS_NOT_EMPTY = 6; // 0x6
-    field public static final int CODE_CANNOT_ADD_MANAGED_PROFILE = 11; // 0xb
-    field public static final int CODE_DEVICE_ADMIN_NOT_SUPPORTED = 13; // 0xd
-    field public static final int CODE_HAS_DEVICE_OWNER = 1; // 0x1
-    field public static final int CODE_HAS_PAIRED = 8; // 0x8
-    field public static final int CODE_MANAGED_USERS_NOT_SUPPORTED = 9; // 0x9
-    field public static final int CODE_NONSYSTEM_USER_EXISTS = 5; // 0x5
-    field public static final int CODE_NOT_SYSTEM_USER = 7; // 0x7
-    field public static final int CODE_OK = 0; // 0x0
-    field public static final int CODE_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS = 15; // 0xf
-    field public static final int CODE_SYSTEM_USER = 10; // 0xa
-    field public static final int CODE_UNKNOWN_ERROR = -1; // 0xffffffff
-    field public static final int CODE_USER_HAS_PROFILE_OWNER = 2; // 0x2
-    field public static final int CODE_USER_NOT_RUNNING = 3; // 0x3
-    field public static final int CODE_USER_SETUP_COMPLETED = 4; // 0x4
     field public static final String EXTRA_FORCE_UPDATE_ROLE_HOLDER = "android.app.extra.FORCE_UPDATE_ROLE_HOLDER";
     field public static final String EXTRA_LOST_MODE_LOCATION = "android.app.extra.LOST_MODE_LOCATION";
     field public static final String EXTRA_PROFILE_OWNER_NAME = "android.app.extra.PROFILE_OWNER_NAME";
@@ -1185,6 +1170,21 @@
     field public static final int STATE_USER_SETUP_FINALIZED = 3; // 0x3
     field public static final int STATE_USER_SETUP_INCOMPLETE = 1; // 0x1
     field public static final int STATE_USER_UNMANAGED = 0; // 0x0
+    field public static final int STATUS_ACCOUNTS_NOT_EMPTY = 6; // 0x6
+    field public static final int STATUS_CANNOT_ADD_MANAGED_PROFILE = 11; // 0xb
+    field public static final int STATUS_DEVICE_ADMIN_NOT_SUPPORTED = 13; // 0xd
+    field public static final int STATUS_HAS_DEVICE_OWNER = 1; // 0x1
+    field public static final int STATUS_HAS_PAIRED = 8; // 0x8
+    field public static final int STATUS_MANAGED_USERS_NOT_SUPPORTED = 9; // 0x9
+    field public static final int STATUS_NONSYSTEM_USER_EXISTS = 5; // 0x5
+    field public static final int STATUS_NOT_SYSTEM_USER = 7; // 0x7
+    field public static final int STATUS_OK = 0; // 0x0
+    field public static final int STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS = 15; // 0xf
+    field public static final int STATUS_SYSTEM_USER = 10; // 0xa
+    field public static final int STATUS_UNKNOWN_ERROR = -1; // 0xffffffff
+    field public static final int STATUS_USER_HAS_PROFILE_OWNER = 2; // 0x2
+    field public static final int STATUS_USER_NOT_RUNNING = 3; // 0x3
+    field public static final int STATUS_USER_SETUP_COMPLETED = 4; // 0x4
   }
 
   public static final class DevicePolicyResources.Strings {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 29b8248..9757b2f 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -525,7 +525,6 @@
     method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public void setNextOperationSafety(int, int);
     field public static final String ACTION_DATA_SHARING_RESTRICTION_APPLIED = "android.app.action.DATA_SHARING_RESTRICTION_APPLIED";
     field public static final String ACTION_DEVICE_POLICY_CONSTANTS_CHANGED = "android.app.action.DEVICE_POLICY_CONSTANTS_CHANGED";
-    field @Deprecated public static final int CODE_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER = 14; // 0xe
     field public static final int DEVICE_OWNER_TYPE_DEFAULT = 0; // 0x0
     field public static final int DEVICE_OWNER_TYPE_FINANCED = 1; // 0x1
     field public static final int OPERATION_CLEAR_APPLICATION_USER_DATA = 23; // 0x17
@@ -569,6 +568,7 @@
     field public static final int OPERATION_SWITCH_USER = 2; // 0x2
     field public static final int OPERATION_UNINSTALL_CA_CERT = 40; // 0x28
     field public static final int OPERATION_WIPE_DATA = 8; // 0x8
+    field @Deprecated public static final int STATUS_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER = 14; // 0xe
   }
 
   public static final class SecurityLog.SecurityEvent implements android.os.Parcelable {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 9a7093e..753df3d 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -2523,7 +2523,7 @@
     public @interface UserProvisioningState {}
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Unknown error code returned  for {@link #ACTION_PROVISION_MANAGED_DEVICE},
      * {@link #ACTION_PROVISION_MANAGED_PROFILE} and {@link #ACTION_PROVISION_MANAGED_USER}.
@@ -2531,10 +2531,10 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_UNKNOWN_ERROR = -1;
+    public static final int STATUS_UNKNOWN_ERROR = -1;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE},
      * {@link #ACTION_PROVISION_MANAGED_PROFILE} and {@link #ACTION_PROVISION_MANAGED_USER}
@@ -2543,10 +2543,10 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_OK = 0;
+    public static final int STATUS_OK = 0;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} when the device already has a
      * device owner.
@@ -2554,10 +2554,10 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_HAS_DEVICE_OWNER = 1;
+    public static final int STATUS_HAS_DEVICE_OWNER = 1;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} when the user has a profile owner
      *  and for {@link #ACTION_PROVISION_MANAGED_PROFILE} when the profile owner is already set.
@@ -2565,20 +2565,20 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_USER_HAS_PROFILE_OWNER = 2;
+    public static final int STATUS_USER_HAS_PROFILE_OWNER = 2;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} when the user isn't running.
      *
      * @hide
      */
     @SystemApi
-    public static final int CODE_USER_NOT_RUNNING = 3;
+    public static final int STATUS_USER_NOT_RUNNING = 3;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} if the device has already been
      * setup and for {@link #ACTION_PROVISION_MANAGED_USER} if the user has already been setup.
@@ -2586,7 +2586,7 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_USER_SETUP_COMPLETED = 4;
+    public static final int STATUS_USER_SETUP_COMPLETED = 4;
 
     /**
      * Code used to indicate that the device also has a user other than the system user.
@@ -2594,7 +2594,7 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_NONSYSTEM_USER_EXISTS = 5;
+    public static final int STATUS_NONSYSTEM_USER_EXISTS = 5;
 
     /**
      * Code used to indicate that device has an account that prevents provisioning.
@@ -2602,20 +2602,20 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_ACCOUNTS_NOT_EMPTY = 6;
+    public static final int STATUS_ACCOUNTS_NOT_EMPTY = 6;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} if the user is not a system user.
      *
      * @hide
      */
     @SystemApi
-    public static final int CODE_NOT_SYSTEM_USER = 7;
+    public static final int STATUS_NOT_SYSTEM_USER = 7;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} and
      * {@link #ACTION_PROVISION_MANAGED_USER} when the device is a watch and is already paired.
@@ -2623,10 +2623,10 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_HAS_PAIRED = 8;
+    public static final int STATUS_HAS_PAIRED = 8;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_PROFILE} and
      * {@link #ACTION_PROVISION_MANAGED_USER} on devices which do not support managed users.
@@ -2635,10 +2635,10 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_MANAGED_USERS_NOT_SUPPORTED = 9;
+    public static final int STATUS_MANAGED_USERS_NOT_SUPPORTED = 9;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_USER} if the user is a system user and
      * for {@link #ACTION_PROVISION_MANAGED_DEVICE} on devices running headless system user mode
@@ -2647,10 +2647,10 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_SYSTEM_USER = 10;
+    public static final int STATUS_SYSTEM_USER = 10;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_PROFILE} when the user cannot have more
      * managed profiles.
@@ -2658,19 +2658,10 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_CANNOT_ADD_MANAGED_PROFILE = 11;
+    public static final int STATUS_CANNOT_ADD_MANAGED_PROFILE = 11;
 
     /**
-     * TODO (b/137101239): clean up split system user codes
-     *
-     * @hide
-     * @deprecated not used anymore but can't be removed since it's a @TestApi.
-     **/
-    @Deprecated
-    public static final int CODE_NOT_SYSTEM_USER_SPLIT = 12;
-
-    /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE},
      * {@link #ACTION_PROVISION_MANAGED_PROFILE} on devices which do not support device
@@ -2679,21 +2670,21 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_DEVICE_ADMIN_NOT_SUPPORTED = 13;
+    public static final int STATUS_DEVICE_ADMIN_NOT_SUPPORTED = 13;
 
     /**
      * TODO (b/137101239): clean up split system user codes
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * @hide
      * @deprecated not used anymore but can't be removed since it's a @TestApi.
      */
     @Deprecated
     @TestApi
-    public static final int CODE_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER = 14;
+    public static final int STATUS_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER = 14;
 
     /**
-     * Result code for {@link #checkProvisioningPreCondition}.
+     * Result code for {@link #checkProvisioningPrecondition}.
      *
      * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} and
      * {@link #ACTION_PROVISION_MANAGED_PROFILE} on devices which do not support provisioning.
@@ -2701,24 +2692,24 @@
      * @hide
      */
     @SystemApi
-    public static final int CODE_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS = 15;
+    public static final int STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS = 15;
 
     /**
-     * Result codes for {@link #checkProvisioningPreCondition} indicating all the provisioning pre
+     * Result codes for {@link #checkProvisioningPrecondition} indicating all the provisioning pre
      * conditions.
      *
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = { "CODE_" }, value = {
-            CODE_UNKNOWN_ERROR, CODE_OK, CODE_HAS_DEVICE_OWNER, CODE_USER_HAS_PROFILE_OWNER,
-            CODE_USER_NOT_RUNNING, CODE_USER_SETUP_COMPLETED, CODE_NOT_SYSTEM_USER, CODE_HAS_PAIRED,
-            CODE_MANAGED_USERS_NOT_SUPPORTED, CODE_SYSTEM_USER, CODE_CANNOT_ADD_MANAGED_PROFILE,
-            CODE_NOT_SYSTEM_USER_SPLIT, CODE_DEVICE_ADMIN_NOT_SUPPORTED,
-            CODE_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER,
-            CODE_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS
+    @IntDef(prefix = { "STATUS_" }, value = {
+            STATUS_UNKNOWN_ERROR, STATUS_OK, STATUS_HAS_DEVICE_OWNER, STATUS_USER_HAS_PROFILE_OWNER,
+            STATUS_USER_NOT_RUNNING, STATUS_USER_SETUP_COMPLETED, STATUS_NOT_SYSTEM_USER,
+            STATUS_HAS_PAIRED, STATUS_MANAGED_USERS_NOT_SUPPORTED, STATUS_SYSTEM_USER,
+            STATUS_CANNOT_ADD_MANAGED_PROFILE, STATUS_DEVICE_ADMIN_NOT_SUPPORTED,
+            STATUS_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER,
+            STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS
     })
-    public @interface ProvisioningPreCondition {}
+    public @interface ProvisioningPrecondition {}
 
     /**
      * Disable all configurable SystemUI features during LockTask mode. This includes,
@@ -11996,15 +11987,16 @@
      *        {@link #ACTION_PROVISION_MANAGED_PROFILE}
      * @param packageName The package of the component that would be set as device, user, or profile
      *        owner.
-     * @return A {@link ProvisioningPreCondition} value indicating whether provisioning is allowed.
+     * @return An int constant value indicating whether provisioning is allowed.
      * @hide
      */
     @SystemApi
     @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS)
-    public @ProvisioningPreCondition int checkProvisioningPreCondition(
+    @ProvisioningPrecondition
+    public int checkProvisioningPrecondition(
             @NonNull String action, @NonNull String packageName) {
         try {
-            return mService.checkProvisioningPreCondition(action, packageName);
+            return mService.checkProvisioningPrecondition(action, packageName);
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -14689,7 +14681,7 @@
      * {@link ManagedProfileProvisioningParams#getProfileAdminComponentName()} as the profile
      * owner.
      *
-     * <p>The method {@link #checkProvisioningPreCondition} must be returning {@link #CODE_OK}
+     * <p>The method {@link #checkProvisioningPrecondition} must be returning {@link #STATUS_OK}
      * before calling this method.
      *
      * @param provisioningParams Params required to provision a managed profile,
@@ -14733,7 +14725,7 @@
      * Provisions a managed device and sets the {@code deviceAdminComponentName} as the device
      * owner.
      *
-     * <p>The method {@link #checkProvisioningPreCondition} must be returning {@link #CODE_OK}
+     * <p>The method {@link #checkProvisioningPrecondition} must be returning {@link #STATUS_OK}
      * before calling this method.
      *
      * @param provisioningParams Params required to provision a fully managed device,
diff --git a/core/java/android/app/admin/DevicePolicyResources.java b/core/java/android/app/admin/DevicePolicyResources.java
index 7f2e5fd..042e407 100644
--- a/core/java/android/app/admin/DevicePolicyResources.java
+++ b/core/java/android/app/admin/DevicePolicyResources.java
@@ -317,6 +317,8 @@
  */
 public final class DevicePolicyResources {
 
+    private DevicePolicyResources() {}
+
     /**
      * Resource identifiers used to update device management-related system drawable resources.
      *
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 0b9d51f..9d28dde 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -373,7 +373,7 @@
             String permission, int grantState, in RemoteCallback resultReceiver);
     int getPermissionGrantState(in ComponentName admin, in String callerPackage, String packageName, String permission);
     boolean isProvisioningAllowed(String action, String packageName);
-    int checkProvisioningPreCondition(String action, String packageName);
+    int checkProvisioningPrecondition(String action, String packageName);
     void setKeepUninstalledPackages(in ComponentName admin, in String callerPackage, in List<String> packageList);
     List<String> getKeepUninstalledPackages(in ComponentName admin, in String callerPackage);
     boolean isManagedProfile(in ComponentName admin);
diff --git a/core/java/android/app/admin/ProvisioningException.java b/core/java/android/app/admin/ProvisioningException.java
index 57a2c50..a457b5f 100644
--- a/core/java/android/app/admin/ProvisioningException.java
+++ b/core/java/android/app/admin/ProvisioningException.java
@@ -46,7 +46,7 @@
     /**
      * Service-specific error code for {@link DevicePolicyManager#provisionFullyManagedDevice} and
      * {@link DevicePolicyManager#createAndProvisionManagedProfile}:
-     * Indicates the call to {@link DevicePolicyManager#checkProvisioningPreCondition} returned an
+     * Indicates the call to {@link DevicePolicyManager#checkProvisioningPrecondition} returned an
      * error code.
      */
     public static final int ERROR_PRE_CONDITION_FAILED = 1;
diff --git a/core/java/android/hardware/SyncFence.java b/core/java/android/hardware/SyncFence.java
index 99791ce..cd4bf78 100644
--- a/core/java/android/hardware/SyncFence.java
+++ b/core/java/android/hardware/SyncFence.java
@@ -17,6 +17,8 @@
 package android.hardware;
 
 import android.annotation.NonNull;
+import android.media.Image;
+import android.media.ImageWriter;
 import android.opengl.EGLDisplay;
 import android.opengl.EGLSync;
 import android.os.Parcel;
@@ -30,14 +32,29 @@
 import java.time.Duration;
 
 /**
- * A SyncFence represents a synchronization primitive which signals when hardware buffers have
- * completed work on a particular resource.
+ * A SyncFence represents a synchronization primitive which signals when hardware units have
+ * completed work on a particular resource. They initially start in an unsignaled state and make
+ * a one-time transition to either a signaled or error state. SyncFences are created by various
+ * device APIs in response to submitting tasks to the device. They cannot be created nor signaled
+ * by userspace. As a result, this means that a SyncFence will make always make forward progress.
+ *
+ * <p>SyncFence's generally come in one of two varieties. "Presentation fences" refer to
+ *  a SyncFence when the writing to a buffer has finished. "Release fences" then refer
+ *  to when the reading from a buffer has finished.</p>
  *
  * <p>For example, a GPU rendering to a framebuffer may generate a synchronization fence,
- * e.g., a VkFence, which signals when rendering has completed.
+ * e.g., an EGLSync or VkFence, which signals when rendering has completed. Once the fence signals,
+ * then the backing storage for the framebuffer may be safely read from, such as for display or
+ * for media encoding. This would be referred to as a "presentation fence."</p>
  *
- * Once the fence signals, then the backing storage for the framebuffer may be safely read from,
- * such as for display or for media encoding.</p>
+ * <p>Similarly when using an {@link android.media.ImageWriter} it is possible that an
+ * {@link android.media.Image} returned by {@link ImageWriter#dequeueInputImage()} may already
+ * have a {@link Image#getFence() fence} set on it. This would be what is referred to as either
+ * a "release fence" or an "acqurie fence" and indicates the fence that the writer must wait
+ * on before writing to the underlying buffer. In the case of ImageWriter this is done
+ * automatically when eg {@link Image#getPlanes()} is called, however when using
+ * {@link Image#getHardwareBuffer()} it is the caller's responsibility to ensure the
+ * release fence has signaled before writing to the buffer.</p>
  *
  * @see android.opengl.EGLExt#eglDupNativeFenceFDANDROID(EGLDisplay, EGLSync)
  * @see android.media.Image#getFence()
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 2fd79cf..c38a847 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1079,16 +1079,24 @@
     }
 
     /**
-     * Gets the key code produced by the specified location on a US keyboard layout.
-     * Key code as defined in {@link android.view.KeyEvent}.
-     * This API is only functional for devices with {@link InputDevice#SOURCE_KEYBOARD} available
-     * which can alter their key mapping using country specific keyboard layouts.
+     * Gets the {@link android.view.KeyEvent key code} produced by the given location on a reference
+     * QWERTY keyboard layout.
+     * <p>
+     * This API is useful for querying the physical location of keys that change the character
+     * produced based on the current locale and keyboard layout.
+     * <p>
+     * @see InputDevice#getKeyCodeForKeyLocation(int) for examples.
      *
-     * @param deviceId The input device id.
-     * @param locationKeyCode The location of a key on a US keyboard layout.
-     * @return The key code produced when pressing the key at the specified location, given the
-     *         active keyboard layout. Returns {@link KeyEvent#KEYCODE_UNKNOWN} if the requested
-     *         mapping could not be determined, or if an error occurred.
+     * @param locationKeyCode The location of a key specified as a key code on the QWERTY layout.
+     * This provides a consistent way of referring to the physical location of a key independently
+     * of the current keyboard layout. Also see the
+     * <a href="https://www.w3.org/TR/2017/CR-uievents-code-20170601/#key-alphanumeric-writing-system">
+     * hypothetical keyboard</a> provided by the W3C, which may be helpful for identifying the
+     * physical location of a key.
+     * @return The key code produced by the key at the specified location, given the current
+     * keyboard layout. Returns {@link KeyEvent#KEYCODE_UNKNOWN} if the device does not specify
+     * {@link InputDevice#SOURCE_KEYBOARD} or the requested mapping cannot be determined.
+     *
      * @hide
      */
     public int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode) {
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index 0a9fe90..9a780c8 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -84,13 +84,6 @@
     public static final String EXTRA_SERVICE_COMPONENT = "component";
 
     /**
-     * The caller userId extra for {@link #ACTION_CHANGE_DEFAULT}.
-     *
-     * @see #ACTION_CHANGE_DEFAULT
-     */
-    public static final String EXTRA_USERID = "android.nfc.cardemulation.extra.USERID";
-
-    /**
      * Category used for NFC payment services.
      */
     public static final String CATEGORY_PAYMENT = "payment";
diff --git a/core/java/android/os/BadTypeParcelableException.java b/core/java/android/os/BadTypeParcelableException.java
new file mode 100644
index 0000000..2ca3bd2
--- /dev/null
+++ b/core/java/android/os/BadTypeParcelableException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.os;
+
+/** Used by Parcel to signal that the type on the payload was not expected by the caller. */
+class BadTypeParcelableException extends BadParcelableException {
+    BadTypeParcelableException(String msg) {
+        super(msg);
+    }
+    BadTypeParcelableException(Exception cause) {
+        super(cause);
+    }
+    BadTypeParcelableException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index 244335d..45812e5 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -16,6 +16,8 @@
 
 package android.os;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -31,7 +33,7 @@
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Set;
-import java.util.function.Function;
+import java.util.function.BiFunction;
 
 /**
  * A mapping from String keys to values of various types. In most cases, you
@@ -254,8 +256,8 @@
         }
         try {
             return getValueAt(0, String.class);
-        } catch (ClassCastException | BadParcelableException e) {
-            typeWarning("getPairValue()", /* value */ null, "String", e);
+        } catch (ClassCastException | BadTypeParcelableException e) {
+            typeWarning("getPairValue()", "String", e);
             return null;
         }
     }
@@ -320,28 +322,46 @@
      * This call should always be made after {@link #unparcel()} or inside a lock after making sure
      * {@code mMap} is not null.
      *
+     * @deprecated Use {@link #getValue(String, Class, Class[])}. This method should only be used in
+     *      other deprecated APIs.
+     *
      * @hide
      */
+    @Deprecated
+    @Nullable
     final Object getValue(String key) {
         return getValue(key, /* clazz */ null);
     }
 
+    /** Same as {@link #getValue(String, Class, Class[])} with no item types. */
+    @Nullable
+    final <T> T getValue(String key, @Nullable Class<T> clazz) {
+        // Avoids allocating Class[0] array
+        return getValue(key, clazz, (Class<?>[]) null);
+    }
+
     /**
-     * Returns the value for key {@code key} for expected return type {@param clazz} (or {@code
+     * Returns the value for key {@code key} for expected return type {@code clazz} (or pass {@code
      * null} for no type check).
      *
+     * For {@code itemTypes}, see {@link Parcel#readValue(int, ClassLoader, Class, Class[])}.
+     *
      * This call should always be made after {@link #unparcel()} or inside a lock after making sure
      * {@code mMap} is not null.
      *
      * @hide
      */
-    final <T> T getValue(String key, @Nullable Class<T> clazz) {
+    @Nullable
+    final <T> T getValue(String key, @Nullable Class<T> clazz, @Nullable Class<?>... itemTypes) {
         int i = mMap.indexOfKey(key);
-        return (i >= 0) ? getValueAt(i, clazz) : null;
+        return (i >= 0) ? getValueAt(i, clazz, itemTypes) : null;
     }
 
     /**
-     * Returns the value for a certain position in the array map.
+     * Returns the value for a certain position in the array map for expected return type {@code
+     * clazz} (or pass {@code null} for no type check).
+     *
+     * For {@code itemTypes}, see {@link Parcel#readValue(int, ClassLoader, Class, Class[])}.
      *
      * This call should always be made after {@link #unparcel()} or inside a lock after making sure
      * {@code mMap} is not null.
@@ -349,11 +369,12 @@
      * @hide
      */
     @SuppressWarnings("unchecked")
-    final <T> T getValueAt(int i, @Nullable Class<T> clazz) {
+    @Nullable
+    final <T> T getValueAt(int i, @Nullable Class<T> clazz, @Nullable Class<?>... itemTypes) {
         Object object = mMap.valueAt(i);
-        if (object instanceof Function<?, ?>) {
+        if (object instanceof BiFunction<?, ?, ?>) {
             try {
-                object = ((Function<Class<?>, ?>) object).apply(clazz);
+                object = ((BiFunction<Class<?>, Class<?>[], ?>) object).apply(clazz, itemTypes);
             } catch (BadParcelableException e) {
                 if (sShouldDefuse) {
                     Log.w(TAG, "Failed to parse item " + mMap.keyAt(i) + ", returning null.", e);
@@ -615,7 +636,11 @@
      *
      * @param key a String key
      * @return an Object, or null
+     *
+     * @deprecated Use the type-safe specific APIs depending on the type of the item to be
+     *      retrieved, eg. {@link #getString(String)}.
      */
+    @Deprecated
     @Nullable
     public Object get(String key) {
         unparcel();
@@ -623,6 +648,32 @@
     }
 
     /**
+     * Returns the object of type {@code clazz} for the given {@code key}, or {@code null} if:
+     * <ul>
+     *     <li>No mapping of the desired type exists for the given key.
+     *     <li>A {@code null} value is explicitly associated with the key.
+     *     <li>The object is not of type {@code clazz}.
+     * </ul>
+     *
+     * <p>Use the more specific APIs where possible, especially in the case of containers such as
+     * lists, since those APIs allow you to specify the type of the items.
+     *
+     * @param key String key
+     * @param clazz The type of the object expected
+     * @return an Object, or null
+     */
+    @Nullable
+    <T> T get(@Nullable String key, @NonNull Class<T> clazz) {
+        unparcel();
+        try {
+            return getValue(key, requireNonNull(clazz));
+        } catch (ClassCastException | BadTypeParcelableException e) {
+            typeWarning(key, clazz.getCanonicalName(), e);
+            return null;
+        }
+    }
+
+    /**
      * Removes any entry with the given key from the mapping of this Bundle.
      *
      * @param key a String key
@@ -1006,7 +1057,7 @@
             sb.append(" but value was a ");
             sb.append(value.getClass().getName());
         } else {
-            sb.append(" but value was of a different type ");
+            sb.append(" but value was of a different type");
         }
         sb.append(".  The default value ");
         sb.append(defaultValue);
@@ -1019,6 +1070,10 @@
         typeWarning(key, value, className, "<null>", e);
     }
 
+    void typeWarning(String key, String className, RuntimeException e) {
+        typeWarning(key, /* value */ null, className, "<null>", e);
+    }
+
     /**
      * Returns the value associated with the given key, or defaultValue if
      * no mapping of the desired type exists for the given key.
@@ -1358,7 +1413,11 @@
      *
      * @param key a String, or null
      * @return a Serializable value, or null
+     *
+     * @deprecated Use {@link #getSerializable(String, Class)}. This method should only be used in
+     *      other deprecated APIs.
      */
+    @Deprecated
     @Nullable
     Serializable getSerializable(@Nullable String key) {
         unparcel();
@@ -1375,6 +1434,36 @@
     }
 
     /**
+     * Returns the value associated with the given key, or {@code null} if:
+     * <ul>
+     *     <li>No mapping of the desired type exists for the given key.
+     *     <li>A {@code null} value is explicitly associated with the key.
+     *     <li>The object is not of type {@code clazz}.
+     * </ul>
+     *
+     * @param key a String, or null
+     * @param clazz The expected class of the returned type
+     * @return a Serializable value, or null
+     */
+    @Nullable
+    <T extends Serializable> T getSerializable(@Nullable String key, @NonNull Class<T> clazz) {
+        return get(key, clazz);
+    }
+
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    <T> ArrayList<T> getArrayList(@Nullable String key, @NonNull Class<T> clazz) {
+        unparcel();
+        try {
+            return getValue(key, ArrayList.class, requireNonNull(clazz));
+        } catch (ClassCastException | BadTypeParcelableException e) {
+            typeWarning(key, "ArrayList<" + clazz.getCanonicalName() + ">", e);
+            return null;
+        }
+    }
+
+    /**
      * Returns the value associated with the given key, or null if
      * no mapping of the desired type exists for the given key or a null
      * value is explicitly associated with the key.
@@ -1384,17 +1473,7 @@
      */
     @Nullable
     ArrayList<Integer> getIntegerArrayList(@Nullable String key) {
-        unparcel();
-        Object o = getValue(key);
-        if (o == null) {
-            return null;
-        }
-        try {
-            return (ArrayList<Integer>) o;
-        } catch (ClassCastException e) {
-            typeWarning(key, o, "ArrayList<Integer>", e);
-            return null;
-        }
+        return getArrayList(key, Integer.class);
     }
 
     /**
@@ -1407,17 +1486,7 @@
      */
     @Nullable
     ArrayList<String> getStringArrayList(@Nullable String key) {
-        unparcel();
-        Object o = getValue(key);
-        if (o == null) {
-            return null;
-        }
-        try {
-            return (ArrayList<String>) o;
-        } catch (ClassCastException e) {
-            typeWarning(key, o, "ArrayList<String>", e);
-            return null;
-        }
+        return getArrayList(key, String.class);
     }
 
     /**
@@ -1430,17 +1499,7 @@
      */
     @Nullable
     ArrayList<CharSequence> getCharSequenceArrayList(@Nullable String key) {
-        unparcel();
-        Object o = getValue(key);
-        if (o == null) {
-            return null;
-        }
-        try {
-            return (ArrayList<CharSequence>) o;
-        } catch (ClassCastException e) {
-            typeWarning(key, o, "ArrayList<CharSequence>", e);
-            return null;
-        }
+        return getArrayList(key, CharSequence.class);
     }
 
     /**
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index 2b13f20..edbbb59 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.util.ArrayMap;
 import android.util.Size;
@@ -876,7 +877,7 @@
     @Nullable
     public Bundle getBundle(@Nullable String key) {
         unparcel();
-        Object o = getValue(key);
+        Object o = mMap.get(key);
         if (o == null) {
             return null;
         }
@@ -899,7 +900,11 @@
      *
      * @param key a String, or {@code null}
      * @return a Parcelable value, or {@code null}
+     *
+     * @deprecated Use the type-safer {@link #getParcelable(String, Class)} starting from Android
+     *      {@link Build.VERSION_CODES#TIRAMISU}.
      */
+    @Deprecated
     @Nullable
     public <T extends Parcelable> T getParcelable(@Nullable String key) {
         unparcel();
@@ -916,30 +921,28 @@
     }
 
     /**
-     * Returns the value associated with the given key, or {@code null} if
-     * no mapping of the desired type exists for the given key or a {@code null}
-     * value is explicitly associated with the key.
+     * Returns the value associated with the given key or {@code null} if:
+     * <ul>
+     *     <li>No mapping of the desired type exists for the given key.
+     *     <li>A {@code null} value is explicitly associated with the key.
+     *     <li>The object is not of type {@code clazz}.
+     * </ul>
      *
      * <p><b>Note: </b> if the expected value is not a class provided by the Android platform,
      * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first.
      * Otherwise, this method might throw an exception or return {@code null}.
      *
      * @param key a String, or {@code null}
-     * @param clazz The type of the object expected or {@code null} for performing no checks.
+     * @param clazz The type of the object expected
      * @return a Parcelable value, or {@code null}
-     *
-     * @hide
      */
     @SuppressWarnings("unchecked")
     @Nullable
     public <T> T getParcelable(@Nullable String key, @NonNull Class<T> clazz) {
-        unparcel();
-        try {
-            return getValue(key, requireNonNull(clazz));
-        } catch (ClassCastException | BadParcelableException e) {
-            typeWarning(key, /* value */ null, "Parcelable", e);
-            return null;
-        }
+        // The reason for not using <T extends Parcelable> is because the caller could provide a
+        // super class to restrict the children that doesn't implement Parcelable itself while the
+        // children do, more details at b/210800751 (same reasoning applies here).
+        return get(key, clazz);
     }
 
     /**
@@ -953,7 +956,11 @@
      *
      * @param key a String, or {@code null}
      * @return a Parcelable[] value, or {@code null}
+     *
+     * @deprecated Use the type-safer {@link #getParcelableArray(String, Class)} starting from
+     *      Android {@link Build.VERSION_CODES#TIRAMISU}.
      */
+    @Deprecated
     @Nullable
     public Parcelable[] getParcelableArray(@Nullable String key) {
         unparcel();
@@ -970,6 +977,39 @@
     }
 
     /**
+     * Returns the value associated with the given key, or {@code null} if:
+     * <ul>
+     *     <li>No mapping of the desired type exists for the given key.
+     *     <li>A {@code null} value is explicitly associated with the key.
+     *     <li>The object is not of type {@code clazz}.
+     * </ul>
+     *
+     * <p><b>Note: </b> if the expected value is not a class provided by the Android platform,
+     * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first.
+     * Otherwise, this method might throw an exception or return {@code null}.
+     *
+     * @param key a String, or {@code null}
+     * @param clazz The type of the items inside the array
+     * @return a Parcelable[] value, or {@code null}
+     */
+    @SuppressLint({"ArrayReturn", "NullableCollection"})
+    @SuppressWarnings("unchecked")
+    @Nullable
+    public <T> T[] getParcelableArray(@Nullable String key, @NonNull Class<T> clazz) {
+        // The reason for not using <T extends Parcelable> is because the caller could provide a
+        // super class to restrict the children that doesn't implement Parcelable itself while the
+        // children do, more details at b/210800751 (same reasoning applies here).
+        unparcel();
+        try {
+            // In Java 12, we can pass clazz.arrayType() instead of Parcelable[] and later casting.
+            return (T[]) getValue(key, Parcelable[].class, requireNonNull(clazz));
+        } catch (ClassCastException | BadTypeParcelableException e) {
+            typeWarning(key, clazz.getCanonicalName() + "[]", e);
+            return null;
+        }
+    }
+
+    /**
      * Returns the value associated with the given key, or {@code null} if
      * no mapping of the desired type exists for the given key or a {@code null}
      * value is explicitly associated with the key.
@@ -980,7 +1020,11 @@
      *
      * @param key a String, or {@code null}
      * @return an ArrayList<T> value, or {@code null}
+     *
+     * @deprecated Use the type-safer {@link #getParcelable(String, Class)} starting from Android
+     *      {@link Build.VERSION_CODES#TIRAMISU}.
      */
+    @Deprecated
     @Nullable
     public <T extends Parcelable> ArrayList<T> getParcelableArrayList(@Nullable String key) {
         unparcel();
@@ -997,14 +1041,43 @@
     }
 
     /**
+     * Returns the value associated with the given key, or {@code null} if:
+     * <ul>
+     *     <li>No mapping of the desired type exists for the given key.
+     *     <li>A {@code null} value is explicitly associated with the key.
+     *     <li>The object is not of type {@code clazz}.
+     * </ul>
+     *
+     * <p><b>Note: </b> if the expected value is not a class provided by the Android platform,
+     * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first.
+     * Otherwise, this method might throw an exception or return {@code null}.
+     *
+     * @param key   a String, or {@code null}
+     * @param clazz The type of the items inside the array list
+     * @return an ArrayList<T> value, or {@code null}
+     */
+    @SuppressLint("NullableCollection")
+    @SuppressWarnings("unchecked")
+    @Nullable
+    public <T> ArrayList<T> getParcelableArrayList(@Nullable String key, @NonNull Class<T> clazz) {
+        // The reason for not using <T extends Parcelable> is because the caller could provide a
+        // super class to restrict the children that doesn't implement Parcelable itself while the
+        // children do, more details at b/210800751 (same reasoning applies here).
+        return getArrayList(key, clazz);
+    }
+
+    /**
      * Returns the value associated with the given key, or null if
      * no mapping of the desired type exists for the given key or a null
      * value is explicitly associated with the key.
      *
      * @param key a String, or null
-     *
      * @return a SparseArray of T values, or null
+     *
+     * @deprecated Use the type-safer {@link #getSparseParcelableArray(String, Class)} starting from
+     *      Android {@link Build.VERSION_CODES#TIRAMISU}.
      */
+    @Deprecated
     @Nullable
     public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(@Nullable String key) {
         unparcel();
@@ -1021,13 +1094,44 @@
     }
 
     /**
+     * Returns the value associated with the given key, or {@code null} if:
+     * <ul>
+     *     <li>No mapping of the desired type exists for the given key.
+     *     <li>A {@code null} value is explicitly associated with the key.
+     *     <li>The object is not of type {@code clazz}.
+     * </ul>
+     *
+     * @param key a String, or null
+     * @return a SparseArray of T values, or null
+     */
+    @SuppressWarnings("unchecked")
+    @Nullable
+    public <T> SparseArray<T> getSparseParcelableArray(@Nullable String key,
+            @NonNull Class<T> clazz) {
+        // The reason for not using <T extends Parcelable> is because the caller could provide a
+        // super class to restrict the children that doesn't implement Parcelable itself while the
+        // children do, more details at b/210800751 (same reasoning applies here).
+        unparcel();
+        try {
+            return (SparseArray<T>) getValue(key, SparseArray.class, requireNonNull(clazz));
+        } catch (ClassCastException | BadTypeParcelableException e) {
+            typeWarning(key, "SparseArray<" + clazz.getCanonicalName() + ">", e);
+            return null;
+        }
+    }
+
+    /**
      * Returns the value associated with the given key, or null if
      * no mapping of the desired type exists for the given key or a null
      * value is explicitly associated with the key.
      *
      * @param key a String, or null
      * @return a Serializable value, or null
+     *
+     * @deprecated Use the type-safer {@link #getSerializable(String, Class)} starting from Android
+     *      {@link Build.VERSION_CODES#TIRAMISU}.
      */
+    @Deprecated
     @Override
     @Nullable
     public Serializable getSerializable(@Nullable String key) {
@@ -1035,6 +1139,24 @@
     }
 
     /**
+     * Returns the value associated with the given key, or {@code null} if:
+     * <ul>
+     *     <li>No mapping of the desired type exists for the given key.
+     *     <li>A {@code null} value is explicitly associated with the key.
+     *     <li>The object is not of type {@code clazz}.
+     * </ul>
+     *
+     * @param key   a String, or null
+     * @param clazz The expected class of the returned type
+     * @return a Serializable value, or null
+     */
+    @Nullable
+    public <T extends Serializable> T getSerializable(@Nullable String key,
+            @NonNull Class<T> clazz) {
+        return super.getSerializable(key, requireNonNull(clazz));
+    }
+
+    /**
      * Returns the value associated with the given key, or null if
      * no mapping of the desired type exists for the given key or a null
      * value is explicitly associated with the key.
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index ae92353..09cfb6e 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -16,6 +16,8 @@
 
 package android.os;
 
+import static com.android.internal.util.Preconditions.checkArgument;
+
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.IntDef;
@@ -65,6 +67,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.IntFunction;
 import java.util.function.Supplier;
@@ -287,26 +290,26 @@
     private static final int VAL_NULL = -1;
     private static final int VAL_STRING = 0;
     private static final int VAL_INTEGER = 1;
-    private static final int VAL_MAP = 2;
+    private static final int VAL_MAP = 2; // length-prefixed
     private static final int VAL_BUNDLE = 3;
-    private static final int VAL_PARCELABLE = 4;
+    private static final int VAL_PARCELABLE = 4; // length-prefixed
     private static final int VAL_SHORT = 5;
     private static final int VAL_LONG = 6;
     private static final int VAL_FLOAT = 7;
     private static final int VAL_DOUBLE = 8;
     private static final int VAL_BOOLEAN = 9;
     private static final int VAL_CHARSEQUENCE = 10;
-    private static final int VAL_LIST  = 11;
-    private static final int VAL_SPARSEARRAY = 12;
+    private static final int VAL_LIST  = 11; // length-prefixed
+    private static final int VAL_SPARSEARRAY = 12; // length-prefixed
     private static final int VAL_BYTEARRAY = 13;
     private static final int VAL_STRINGARRAY = 14;
     private static final int VAL_IBINDER = 15;
-    private static final int VAL_PARCELABLEARRAY = 16;
-    private static final int VAL_OBJECTARRAY = 17;
+    private static final int VAL_PARCELABLEARRAY = 16; // length-prefixed
+    private static final int VAL_OBJECTARRAY = 17; // length-prefixed
     private static final int VAL_INTARRAY = 18;
     private static final int VAL_LONGARRAY = 19;
     private static final int VAL_BYTE = 20;
-    private static final int VAL_SERIALIZABLE = 21;
+    private static final int VAL_SERIALIZABLE = 21; // length-prefixed
     private static final int VAL_SPARSEBOOLEANARRAY = 22;
     private static final int VAL_BOOLEANARRAY = 23;
     private static final int VAL_CHARSEQUENCEARRAY = 24;
@@ -3179,8 +3182,7 @@
      */
     @Deprecated
     public final void readMap(@NonNull Map outVal, @Nullable ClassLoader loader) {
-        int n = readInt();
-        readMapInternal(outVal, n, loader, /* clazzKey */ null, /* clazzValue */ null);
+        readMapInternal(outVal, loader, /* clazzKey */ null, /* clazzValue */ null);
     }
 
     /**
@@ -3195,8 +3197,7 @@
             @NonNull Class<V> clazzValue) {
         Objects.requireNonNull(clazzKey);
         Objects.requireNonNull(clazzValue);
-        int n = readInt();
-        readMapInternal(outVal, n, loader, clazzKey, clazzValue);
+        readMapInternal(outVal, loader, clazzKey, clazzValue);
     }
 
     /**
@@ -3245,13 +3246,7 @@
     @Deprecated
     @Nullable
     public HashMap readHashMap(@Nullable ClassLoader loader) {
-        int n = readInt();
-        if (n < 0) {
-            return null;
-        }
-        HashMap m = new HashMap(n);
-        readMapInternal(m, n, loader, /* clazzKey */ null, /* clazzValue */ null);
-        return m;
+        return readHashMapInternal(loader, /* clazzKey */ null, /* clazzValue */ null);
     }
 
     /**
@@ -3267,13 +3262,7 @@
             @NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) {
         Objects.requireNonNull(clazzKey);
         Objects.requireNonNull(clazzValue);
-        int n = readInt();
-        if (n < 0) {
-            return null;
-        }
-        HashMap<K, V> map = new HashMap<>(n);
-        readMapInternal(map, n, loader, clazzKey, clazzValue);
-        return map;
+        return readHashMapInternal(loader, clazzKey, clazzValue);
     }
 
     /**
@@ -4296,16 +4285,17 @@
 
 
     /**
-     * @param clazz The type of the object expected or {@code null} for performing no checks.
+     * @see #readValue(int, ClassLoader, Class, Class[])
      */
     @Nullable
-    private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz) {
+    private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz,
+            @Nullable Class<?>... itemTypes) {
         int type = readInt();
         final T object;
         if (isLengthPrefixed(type)) {
             int length = readInt();
             int start = dataPosition();
-            object = readValue(type, loader, clazz);
+            object = readValue(type, loader, clazz, itemTypes);
             int actual = dataPosition() - start;
             if (actual != length) {
                 Slog.wtfStack(TAG,
@@ -4313,25 +4303,26 @@
                                 + "  consumed " + actual + " bytes, but " + length + " expected.");
             }
         } else {
-            object = readValue(type, loader, clazz);
+            object = readValue(type, loader, clazz, itemTypes);
         }
         return object;
     }
 
     /**
-     * This will return a {@link Function} for length-prefixed types that deserializes the object
-     * when {@link Function#apply} is called with the expected class of the return object (or {@code
-     * null} for no type check), for other types it will return the object itself.
+     * This will return a {@link BiFunction} for length-prefixed types that deserializes the object
+     * when {@link BiFunction#apply} is called (the arguments correspond to the ones of {@link
+     * #readValue(int, ClassLoader, Class, Class[])} after the class loader), for other types it
+     * will return the object itself.
      *
-     * <p>After calling {@link Function#apply(Object)} the parcel cursor will not change. Note that
-     * you shouldn't recycle the parcel, not at least until all objects have been retrieved. No
+     * <p>After calling {@link BiFunction#apply} the parcel cursor will not change. Note that you
+     * shouldn't recycle the parcel, not at least until all objects have been retrieved. No
      * synchronization attempts are made.
      *
      * </p>The function returned implements {@link #equals(Object)} and {@link #hashCode()}. Two
      * function objects are equal if either of the following is true:
      * <ul>
-     *   <li>{@link Function#apply} has been called on both and both objects returned are equal.
-     *   <li>{@link Function#apply} hasn't been called on either one and everything below is true:
+     *   <li>{@link BiFunction#apply} has been called on both and both objects returned are equal.
+     *   <li>{@link BiFunction#apply} hasn't been called on either one and everything below is true:
      *   <ul>
      *       <li>The {@code loader} parameters used to retrieve each are equal.
      *       <li>They both have the same type.
@@ -4358,7 +4349,7 @@
     }
 
 
-    private static final class LazyValue implements Function<Class<?>, Object> {
+    private static final class LazyValue implements BiFunction<Class<?>, Class<?>[], Object> {
         /**
          *                      |   4B   |   4B   |
          * mSource = Parcel{... |  type  | length | object | ...}
@@ -4390,7 +4381,7 @@
         }
 
         @Override
-        public Object apply(@Nullable Class<?> clazz) {
+        public Object apply(@Nullable Class<?> clazz, @Nullable Class<?>[] itemTypes) {
             Parcel source = mSource;
             if (source != null) {
                 synchronized (source) {
@@ -4399,7 +4390,7 @@
                         int restore = source.dataPosition();
                         try {
                             source.setDataPosition(mPosition);
-                            mObject = source.readValue(mLoader, clazz);
+                            mObject = source.readValue(mLoader, clazz, itemTypes);
                         } finally {
                             source.setDataPosition(restore);
                         }
@@ -4479,14 +4470,25 @@
         }
     }
 
+    /** Same as {@link #readValue(ClassLoader, Class, Class[])} without any item types. */
+    private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
+        // Avoids allocating Class[0] array
+        return readValue(type, loader, clazz, (Class<?>[]) null);
+    }
+
     /**
      * Reads a value from the parcel of type {@code type}. Does NOT read the int representing the
      * type first.
+     *
      * @param clazz The type of the object expected or {@code null} for performing no checks.
+     * @param itemTypes If the value is a container, these represent the item types (eg. for a list
+     *                  it's the item type, for a map, it's the key type, followed by the value
+     *                  type).
      */
     @SuppressWarnings("unchecked")
     @Nullable
-    private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
+    private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz,
+            @Nullable Class<?>... itemTypes) {
         final Object object;
         switch (type) {
             case VAL_NULL:
@@ -4502,7 +4504,11 @@
                 break;
 
             case VAL_MAP:
-                object = readHashMap(loader);
+                checkTypeToUnparcel(clazz, HashMap.class);
+                Class<?> keyType = ArrayUtils.getOrNull(itemTypes, 0);
+                Class<?> valueType = ArrayUtils.getOrNull(itemTypes, 1);
+                checkArgument((keyType == null) == (valueType == null));
+                object = readHashMapInternal(loader, keyType, valueType);
                 break;
 
             case VAL_PARCELABLE:
@@ -4533,10 +4539,12 @@
                 object = readCharSequence();
                 break;
 
-            case VAL_LIST:
-                object = readArrayList(loader);
+            case VAL_LIST: {
+                checkTypeToUnparcel(clazz, ArrayList.class);
+                Class<?> itemType = ArrayUtils.getOrNull(itemTypes, 0);
+                object = readArrayListInternal(loader, itemType);
                 break;
-
+            }
             case VAL_BOOLEANARRAY:
                 object = createBooleanArray();
                 break;
@@ -4557,10 +4565,12 @@
                 object = readStrongBinder();
                 break;
 
-            case VAL_OBJECTARRAY:
-                object = readArray(loader);
+            case VAL_OBJECTARRAY: {
+                Class<?> itemType = ArrayUtils.getOrNull(itemTypes, 0);
+                checkArrayTypeToUnparcel(clazz, (itemType != null) ? itemType : Object.class);
+                object = readArrayInternal(loader, itemType);
                 break;
-
+            }
             case VAL_INTARRAY:
                 object = createIntArray();
                 break;
@@ -4577,14 +4587,18 @@
                 object = readSerializableInternal(loader, clazz);
                 break;
 
-            case VAL_PARCELABLEARRAY:
-                object = readParcelableArray(loader);
+            case VAL_PARCELABLEARRAY: {
+                Class<?> itemType = ArrayUtils.getOrNull(itemTypes, 0);
+                checkArrayTypeToUnparcel(clazz, (itemType != null) ? itemType : Parcelable.class);
+                object = readParcelableArrayInternal(loader, itemType);
                 break;
-
-            case VAL_SPARSEARRAY:
-                object = readSparseArray(loader);
+            }
+            case VAL_SPARSEARRAY: {
+                checkTypeToUnparcel(clazz, SparseArray.class);
+                Class<?> itemType = ArrayUtils.getOrNull(itemTypes, 0);
+                object = readSparseArrayInternal(loader, itemType);
                 break;
-
+            }
             case VAL_SPARSEBOOLEANARRAY:
                 object = readSparseBooleanArray();
                 break;
@@ -4632,7 +4646,7 @@
                             + " at offset " + off);
         }
         if (object != null && clazz != null && !clazz.isInstance(object)) {
-            throw new BadParcelableException("Unparcelled object " + object
+            throw new BadTypeParcelableException("Unparcelled object " + object
                     + " is not an instance of required class " + clazz.getName()
                     + " provided in the parameter");
         }
@@ -4659,6 +4673,38 @@
     }
 
     /**
+     * Checks that an array of type T[], where T is {@code componentTypeToUnparcel}, is a subtype of
+     * {@code requiredArrayType}.
+     */
+    private void checkArrayTypeToUnparcel(@Nullable Class<?> requiredArrayType,
+            Class<?> componentTypeToUnparcel) {
+        if (requiredArrayType != null) {
+            // In Java 12, we could use componentTypeToUnparcel.arrayType() for the check
+            Class<?> requiredComponentType = requiredArrayType.getComponentType();
+            if (requiredComponentType == null) {
+                throw new BadTypeParcelableException(
+                        "About to unparcel an array but type "
+                                + requiredArrayType.getCanonicalName()
+                                + " required by caller is not an array.");
+            }
+            checkTypeToUnparcel(requiredComponentType, componentTypeToUnparcel);
+        }
+    }
+
+    /**
+     * Checks that {@code typeToUnparcel} is a subtype of {@code requiredType}, if {@code
+     * requiredType} is not {@code null}.
+     */
+    private void checkTypeToUnparcel(@Nullable Class<?> requiredType, Class<?> typeToUnparcel) {
+        if (requiredType != null && !requiredType.isAssignableFrom(typeToUnparcel)) {
+            throw new BadTypeParcelableException(
+                    "About to unparcel a " + typeToUnparcel.getCanonicalName()
+                            + ", which is not a subtype of type " + requiredType.getCanonicalName()
+                            + " required by caller.");
+        }
+    }
+
+    /**
      * Read and return a new Parcelable from the parcel.  The given class loader
      * will be used to load any enclosed Parcelables.  If it is null, the default
      * class loader will be used.
@@ -4788,7 +4834,7 @@
             if (clazz != null) {
                 Class<?> parcelableClass = creator.getClass().getEnclosingClass();
                 if (!clazz.isAssignableFrom(parcelableClass)) {
-                    throw new BadParcelableException("Parcelable creator " + name + " is not "
+                    throw new BadTypeParcelableException("Parcelable creator " + name + " is not "
                             + "a subclass of required class " + clazz.getName()
                             + " provided in the parameter");
                 }
@@ -4811,7 +4857,7 @@
             }
             if (clazz != null) {
                 if (!clazz.isAssignableFrom(parcelableClass)) {
-                    throw new BadParcelableException("Parcelable creator " + name + " is not "
+                    throw new BadTypeParcelableException("Parcelable creator " + name + " is not "
                             + "a subclass of required class " + clazz.getName()
                             + " provided in the parameter");
                 }
@@ -4872,15 +4918,7 @@
     @Deprecated
     @Nullable
     public Parcelable[] readParcelableArray(@Nullable ClassLoader loader) {
-        int N = readInt();
-        if (N < 0) {
-            return null;
-        }
-        Parcelable[] p = new Parcelable[N];
-        for (int i = 0; i < N; i++) {
-            p[i] = readParcelable(loader);
-        }
-        return p;
+        return readParcelableArrayInternal(loader, /* clazz */ null);
     }
 
     /**
@@ -4892,14 +4930,20 @@
      * trying to instantiate an element.
      */
     @SuppressLint({"ArrayReturn", "NullableCollection"})
-    @SuppressWarnings("unchecked")
     @Nullable
     public <T> T[] readParcelableArray(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+        return readParcelableArrayInternal(loader, requireNonNull(clazz));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    private <T> T[] readParcelableArrayInternal(@Nullable ClassLoader loader,
+            @Nullable Class<T> clazz) {
         int n = readInt();
         if (n < 0) {
             return null;
         }
-        T[] p = (T[]) Array.newInstance(clazz, n);
+        T[] p = (T[]) ((clazz == null) ? new Parcelable[n] : Array.newInstance(clazz, n));
         for (int i = 0; i < n; i++) {
             p[i] = readParcelableInternal(loader, clazz);
         }
@@ -4962,7 +5006,7 @@
                 // the class the same way as ObjectInputStream, using the provided classloader.
                 Class<?> cl = Class.forName(name, false, loader);
                 if (!clazz.isAssignableFrom(cl)) {
-                    throw new BadParcelableException("Serializable object "
+                    throw new BadTypeParcelableException("Serializable object "
                             + cl.getName() + " is not a subclass of required class "
                             + clazz.getName() + " provided in the parameter");
                 }
@@ -4987,7 +5031,7 @@
                 // the deserialized object, as we cannot resolve the class the same way as
                 // ObjectInputStream.
                 if (!clazz.isAssignableFrom(object.getClass())) {
-                    throw new BadParcelableException("Serializable object "
+                    throw new BadTypeParcelableException("Serializable object "
                             + object.getClass().getName() + " is not a subclass of required class "
                             + clazz.getName() + " provided in the parameter");
                 }
@@ -5097,7 +5141,26 @@
         readMapInternal(outVal, n, loader, /* clazzKey */null, /* clazzValue */null);
     }
 
-    /* package */ <K, V> void readMapInternal(@NonNull Map<? super K, ? super V> outVal, int n,
+    @Nullable
+    private <K, V> HashMap<K, V> readHashMapInternal(@Nullable ClassLoader loader,
+            @NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) {
+        int n = readInt();
+        if (n < 0) {
+            return null;
+        }
+        HashMap<K, V> map = new HashMap<>(n);
+        readMapInternal(map, n, loader, clazzKey, clazzValue);
+        return map;
+    }
+
+    private <K, V> void readMapInternal(@NonNull Map<? super K, ? super V> outVal,
+            @Nullable ClassLoader loader, @Nullable Class<K> clazzKey,
+            @Nullable Class<V> clazzValue) {
+        int n = readInt();
+        readMapInternal(outVal, n, loader, clazzKey, clazzValue);
+    }
+
+    private <K, V> void readMapInternal(@NonNull Map<? super K, ? super V> outVal, int n,
             @Nullable ClassLoader loader, @Nullable Class<K> clazzKey,
             @Nullable Class<V> clazzValue) {
         while (n > 0) {
@@ -5108,7 +5171,7 @@
         }
     }
 
-    /* package */ void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal,
+    private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal,
             int size, @Nullable ClassLoader loader) {
         readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loader);
     }
diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java
index 3fc9f6b..b48b525 100644
--- a/core/java/android/view/ImeFocusController.java
+++ b/core/java/android/view/ImeFocusController.java
@@ -54,13 +54,11 @@
 
     @NonNull
     private InputMethodManagerDelegate getImmDelegate() {
-        InputMethodManagerDelegate delegate = mDelegate;
-        if (delegate != null) {
-            return delegate;
+        if (mDelegate == null) {
+            mDelegate = mViewRootImpl.mContext.getSystemService(
+                    InputMethodManager.class).getDelegate();
         }
-        delegate = mViewRootImpl.mContext.getSystemService(InputMethodManager.class).getDelegate();
-        mDelegate = delegate;
-        return delegate;
+        return mDelegate;
     }
 
     /** Called when the view root is moved to a different display. */
diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java
index 188d745..7d56039 100644
--- a/core/java/android/view/InputDevice.java
+++ b/core/java/android/view/InputDevice.java
@@ -737,15 +737,47 @@
     }
 
     /**
-     * Gets the key code produced by the specified location on a US keyboard layout.
-     * Key code as defined in {@link android.view.KeyEvent}.
-     * This API is only functional for devices with {@link InputDevice#SOURCE_KEYBOARD} available
-     * which can alter their key mapping using country specific keyboard layouts.
+     * Gets the {@link android.view.KeyEvent key code} produced by the given location on a reference
+     * QWERTY keyboard layout.
+     * <p>
+     * This API is useful for querying the physical location of keys that change the character
+     * produced based on the current locale and keyboard layout.
+     * <p>
+     * The following table provides a non-exhaustive list of examples:
+     * <table border="2" width="85%" align="center" cellpadding="5">
+     *     <thead>
+     *         <tr><th>Active Keyboard Layout</th> <th>Input Parameter</th>
+     *         <th>Return Value</th></tr>
+     *     </thead>
      *
-     * @param locationKeyCode The location of a key on a US keyboard layout.
-     * @return The key code produced when pressing the key at the specified location, given the
-     *         active keyboard layout. Returns {@link KeyEvent#KEYCODE_UNKNOWN} if the requested
-     *         mapping could not be determined, or if an error occurred.
+     *     <tbody>
+     *     <tr>
+     *         <td>French AZERTY</td>
+     *         <td><code>{@link KeyEvent#KEYCODE_Q}</code></td>
+     *         <td><code>{@link KeyEvent#KEYCODE_A}</code></td>
+     *     </tr>
+     *     <tr>
+     *         <td>German QWERTZ</td>
+     *         <td><code>{@link KeyEvent#KEYCODE_Y}</code></td>
+     *         <td><code>{@link KeyEvent#KEYCODE_Z}</code></td>
+     *     </tr>
+     *     <tr>
+     *         <td>US QWERTY</td>
+     *         <td><code>{@link KeyEvent#KEYCODE_B}</code></td>
+     *         <td><code>{@link KeyEvent#KEYCODE_B}</code></td>
+     *     </tr>
+     *     </tbody>
+     * </table>
+     *
+     * @param locationKeyCode The location of a key specified as a key code on the QWERTY layout.
+     * This provides a consistent way of referring to the physical location of a key independently
+     * of the current keyboard layout. Also see the
+     * <a href="https://www.w3.org/TR/2017/CR-uievents-code-20170601/#key-alphanumeric-writing-system">
+     * hypothetical keyboard</a> provided by the W3C, which may be helpful for identifying the
+     * physical location of a key.
+     * @return The key code produced by the key at the specified location, given the current
+     * keyboard layout. Returns {@link KeyEvent#KEYCODE_UNKNOWN} if the device does not specify
+     * {@link InputDevice#SOURCE_KEYBOARD} or the requested mapping cannot be determined.
      */
     public int getKeyCodeForKeyLocation(int locationKeyCode) {
         return InputManager.getInstance().getKeyCodeForKeyLocation(mId, locationKeyCode);
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 98cef958..632af23 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -3094,6 +3094,10 @@
          * @param destFrame The destination rectangle in parent space. Or null for the source frame.
          * @param orientation The buffer rotation
          * @return This transaction object.
+         * @deprecated Use {@link #setCrop(SurfaceControl, Rect)},
+         * {@link #setBufferTransform(SurfaceControl, int)},
+         * {@link #setPosition(SurfaceControl, float, float)} and
+         * {@link #setScale(SurfaceControl, float, float)} instead.
          */
         @NonNull
         public Transaction setGeometry(@NonNull SurfaceControl sc, @Nullable Rect sourceCrop,
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 286b502..34a1386 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -14173,7 +14173,7 @@
                 && isAccessibilityPane()) {
             // If the pane isn't visible, content changed events are sufficient unless we're
             // reporting that the view just disappeared
-            if ((getVisibility() == VISIBLE)
+            if ((isAggregatedVisible())
                     || (changeType == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED)) {
                 final AccessibilityEvent event = AccessibilityEvent.obtain();
                 onInitializeAccessibilityEvent(event);
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 1c27046..8ee3e43 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3222,7 +3222,8 @@
 
         /**
          * The window is allowed to extend into the {@link DisplayCutout} area, only if the
-         * {@link DisplayCutout} is fully contained within a system bar. Otherwise, the window is
+         * {@link DisplayCutout} is fully contained within a system bar or the {@link DisplayCutout}
+         * is not deeper than 16 dp, but this depends on the OEM choice. Otherwise, the window is
          * laid out such that it does not overlap with the {@link DisplayCutout} area.
          *
          * <p>
@@ -3237,6 +3238,13 @@
          * The usual precautions for not overlapping with the status and navigation bar are
          * sufficient for ensuring that no important content overlaps with the DisplayCutout.
          *
+         * <p>
+         * Note: OEMs can have an option to allow the window to always extend into the
+         * {@link DisplayCutout} area, no matter the cutout flag set, when the {@link DisplayCutout}
+         * is on the different side from system bars, only if the {@link DisplayCutout} overlaps at
+         * most 16dp with the windows.
+         * In such case, OEMs must provide an opt-in/out affordance for users.
+         *
          * @see DisplayCutout
          * @see WindowInsets
          * @see #layoutInDisplayCutoutMode
@@ -3249,8 +3257,16 @@
          * The window is always allowed to extend into the {@link DisplayCutout} areas on the short
          * edges of the screen.
          *
+         * <p>
          * The window will never extend into a {@link DisplayCutout} area on the long edges of the
-         * screen.
+         * screen, unless the {@link DisplayCutout} is not deeper than 16 dp, but this depends on
+         * the OEM choice.
+         *
+         * <p>
+         * Note: OEMs can have an option to allow the window to extend into the
+         * {@link DisplayCutout} area on the long edge side, only if the cutout overlaps at most
+         * 16dp with the windows. In such case, OEMs must provide an opt-in/out affordance for
+         * users.
          *
          * <p>
          * The window must make sure that no important content overlaps with the
diff --git a/core/java/android/webkit/UserPackage.java b/core/java/android/webkit/UserPackage.java
index 5bcfa8b..1da2af4 100644
--- a/core/java/android/webkit/UserPackage.java
+++ b/core/java/android/webkit/UserPackage.java
@@ -34,7 +34,7 @@
     private final UserInfo mUserInfo;
     private final PackageInfo mPackageInfo;
 
-    public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.S;
+    public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.TIRAMISU;
 
     public UserPackage(UserInfo user, PackageInfo packageInfo) {
         this.mUserInfo = user;
diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java
index cf6807e..0ef37d1 100644
--- a/core/java/android/webkit/WebViewFactory.java
+++ b/core/java/android/webkit/WebViewFactory.java
@@ -51,7 +51,7 @@
     // visible for WebViewZygoteInit to look up the class by reflection and call preloadInZygote.
     /** @hide */
     private static final String CHROMIUM_WEBVIEW_FACTORY =
-            "com.android.webview.chromium.WebViewChromiumFactoryProviderForS";
+            "com.android.webview.chromium.WebViewChromiumFactoryProviderForT";
 
     private static final String CHROMIUM_WEBVIEW_FACTORY_METHOD = "create";
 
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 3dfb4a5..c207af5 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -12774,7 +12774,9 @@
     /**
      * Called when a context menu option for the text view is selected.  Currently
      * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
-     * {@link android.R.id#copy}, {@link android.R.id#paste} or {@link android.R.id#shareText}.
+     * {@link android.R.id#copy}, {@link android.R.id#paste},
+     * {@link android.R.id#pasteAsPlainText} (starting at API level 23) or
+     * {@link android.R.id#shareText}.
      *
      * @return true if the context menu item action was performed.
      */
@@ -12965,6 +12967,7 @@
      * method. The default actions can also be removed from the menu using
      * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll},
      * {@link android.R.id#cut}, {@link android.R.id#copy}, {@link android.R.id#paste},
+     * {@link android.R.id#pasteAsPlainText} (starting at API level 23),
      * {@link android.R.id#replaceText} or {@link android.R.id#shareText} ids as parameters.
      *
      * <p>Returning false from
@@ -13003,7 +13006,8 @@
      * {@link android.view.ActionMode.Callback#onPrepareActionMode(android.view.ActionMode,
      * android.view.Menu)} method. The default actions can also be removed from the menu using
      * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll},
-     * {@link android.R.id#paste} or {@link android.R.id#replaceText} ids as parameters.</p>
+     * {@link android.R.id#paste}, {@link android.R.id#pasteAsPlainText} (starting at API
+     * level 23) or {@link android.R.id#replaceText} ids as parameters.</p>
      *
      * <p>Returning false from
      * {@link android.view.ActionMode.Callback#onCreateActionMode(android.view.ActionMode,
diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialog.java b/core/java/com/android/internal/app/MediaRouteChooserDialog.java
index 7108d14..23d966fd 100644
--- a/core/java/com/android/internal/app/MediaRouteChooserDialog.java
+++ b/core/java/com/android/internal/app/MediaRouteChooserDialog.java
@@ -16,25 +16,26 @@
 
 package com.android.internal.app;
 
-import com.android.internal.R;
-
-import android.app.Dialog;
+import android.app.AlertDialog;
 import android.content.Context;
 import android.media.MediaRouter;
 import android.media.MediaRouter.RouteInfo;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.TypedValue;
+import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.Window;
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.Button;
+import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.TextView;
 
+import com.android.internal.R;
+
 import java.util.Comparator;
 
 /**
@@ -48,9 +49,10 @@
  *
  * TODO: Move this back into the API, as in the support library media router.
  */
-public class MediaRouteChooserDialog extends Dialog {
+public class MediaRouteChooserDialog extends AlertDialog {
     private final MediaRouter mRouter;
     private final MediaRouterCallback mCallback;
+    private final boolean mShowProgressBarWhenEmpty;
 
     private int mRouteTypes;
     private View.OnClickListener mExtendedSettingsClickListener;
@@ -60,10 +62,15 @@
     private boolean mAttachedToWindow;
 
     public MediaRouteChooserDialog(Context context, int theme) {
+        this(context, theme, true);
+    }
+
+    public MediaRouteChooserDialog(Context context, int theme, boolean showProgressBarWhenEmpty) {
         super(context, theme);
 
         mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
         mCallback = new MediaRouterCallback();
+        mShowProgressBarWhenEmpty = showProgressBarWhenEmpty;
     }
 
     /**
@@ -120,28 +127,38 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
+        // Note: setView must be called before super.onCreate().
+        setView(LayoutInflater.from(getContext()).inflate(R.layout.media_route_chooser_dialog,
+                null));
 
-        getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
-
-        setContentView(R.layout.media_route_chooser_dialog);
         setTitle(mRouteTypes == MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY
                 ? R.string.media_route_chooser_title_for_remote_display
                 : R.string.media_route_chooser_title);
 
-        // Must be called after setContentView.
-        getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
-                isLightTheme(getContext()) ? R.drawable.ic_media_route_off_holo_light
-                    : R.drawable.ic_media_route_off_holo_dark);
+        setIcon(isLightTheme(getContext()) ? R.drawable.ic_media_route_off_holo_light
+                : R.drawable.ic_media_route_off_holo_dark);
 
+        super.onCreate(savedInstanceState);
+
+        View emptyView = findViewById(android.R.id.empty);
         mAdapter = new RouteAdapter(getContext());
-        mListView = (ListView)findViewById(R.id.media_route_list);
+        mListView = (ListView) findViewById(R.id.media_route_list);
         mListView.setAdapter(mAdapter);
         mListView.setOnItemClickListener(mAdapter);
-        mListView.setEmptyView(findViewById(android.R.id.empty));
+        mListView.setEmptyView(emptyView);
 
-        mExtendedSettingsButton = (Button)findViewById(R.id.media_route_extended_settings_button);
+        mExtendedSettingsButton = (Button) findViewById(R.id.media_route_extended_settings_button);
         updateExtendedSettingsButton();
+
+        if (!mShowProgressBarWhenEmpty) {
+            findViewById(R.id.media_route_progress_bar).setVisibility(View.GONE);
+
+            // Center the empty view when the progress bar is not shown.
+            LinearLayout.LayoutParams params =
+                    (LinearLayout.LayoutParams) emptyView.getLayoutParams();
+            params.gravity = Gravity.CENTER;
+            emptyView.setLayoutParams(params);
+        }
     }
 
     private void updateExtendedSettingsButton() {
diff --git a/core/java/com/android/internal/app/MediaRouteDialogPresenter.java b/core/java/com/android/internal/app/MediaRouteDialogPresenter.java
index bb2d7fa..5628b7e 100644
--- a/core/java/com/android/internal/app/MediaRouteDialogPresenter.java
+++ b/core/java/com/android/internal/app/MediaRouteDialogPresenter.java
@@ -66,24 +66,37 @@
         }
     }
 
+    /** Create a media route dialog as appropriate. */
     public static Dialog createDialog(Context context,
             int routeTypes, View.OnClickListener extendedSettingsClickListener) {
-        final MediaRouter router = (MediaRouter)context.getSystemService(
-                Context.MEDIA_ROUTER_SERVICE);
-
         int theme = MediaRouteChooserDialog.isLightTheme(context)
                 ? android.R.style.Theme_DeviceDefault_Light_Dialog
                 : android.R.style.Theme_DeviceDefault_Dialog;
+        return createDialog(context, routeTypes, extendedSettingsClickListener, theme);
+    }
+
+    /** Create a media route dialog as appropriate. */
+    public static Dialog createDialog(Context context,
+            int routeTypes, View.OnClickListener extendedSettingsClickListener, int theme) {
+        return createDialog(context, routeTypes, extendedSettingsClickListener, theme,
+                true /* showProgressBarWhenEmpty */);
+    }
+
+    /** Create a media route dialog as appropriate. */
+    public static Dialog createDialog(Context context, int routeTypes,
+            View.OnClickListener extendedSettingsClickListener, int theme,
+            boolean showProgressBarWhenEmpty) {
+        final MediaRouter router = context.getSystemService(MediaRouter.class);
 
         MediaRouter.RouteInfo route = router.getSelectedRoute();
         if (route.isDefault() || !route.matchesTypes(routeTypes)) {
-            final MediaRouteChooserDialog d = new MediaRouteChooserDialog(context, theme);
+            final MediaRouteChooserDialog d = new MediaRouteChooserDialog(context, theme,
+                    showProgressBarWhenEmpty);
             d.setRouteTypes(routeTypes);
             d.setExtendedSettingsClickListener(extendedSettingsClickListener);
             return d;
         } else {
-            MediaRouteControllerDialog d = new MediaRouteControllerDialog(context, theme);
-            return d;
+            return new MediaRouteControllerDialog(context, theme);
         }
     }
 }
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index 9bdcddf..1fd0410 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -888,6 +888,15 @@
         }
     }
 
+    /**
+     * Returns the {@code i}-th item in {@code items}, if it exists and {@code items} is not {@code
+     * null}, otherwise returns {@code null}.
+     */
+    @Nullable
+    public static <T> T getOrNull(@Nullable T[] items, int i) {
+        return (items != null && items.length > i) ? items[i] : null;
+    }
+
     public static @Nullable <T> T firstOrNull(T[] items) {
         return items.length > 0 ? items[0] : null;
     }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d4c03e4..80317b8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6912,6 +6912,10 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
+        <service android:name="com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceService"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+
         <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
             android:exported="false">
             <intent-filter>
diff --git a/core/res/res/layout/media_route_chooser_dialog.xml b/core/res/res/layout/media_route_chooser_dialog.xml
index cd1c74f..bf73f4b 100644
--- a/core/res/res/layout/media_route_chooser_dialog.xml
+++ b/core/res/res/layout/media_route_chooser_dialog.xml
@@ -28,20 +28,21 @@
 
     <!-- Content to show when list is empty. -->
     <LinearLayout android:id="@android:id/empty"
-              android:layout_width="match_parent"
-              android:layout_height="64dp"
-              android:orientation="horizontal"
-              android:paddingLeft="16dp"
-              android:paddingRight="16dp"
-              android:visibility="gone">
-        <ProgressBar android:layout_width="wrap_content"
-                     android:layout_height="wrap_content"
-                     android:layout_gravity="center" />
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:paddingLeft="16dp"
+        android:paddingRight="16dp"
+        android:visibility="gone">
+        <ProgressBar android:id="@+id/media_route_progress_bar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center" />
         <TextView android:layout_width="wrap_content"
-                  android:layout_height="wrap_content"
-                  android:layout_gravity="center"
-                  android:paddingStart="16dp"
-                  android:text="@string/media_route_chooser_searching" />
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:paddingStart="16dp"
+            android:text="@string/media_route_chooser_searching" />
     </LinearLayout>
 
     <!-- Settings button. -->
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 7150fca..689d37c 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7013,30 +7013,42 @@
 
     <!-- Defines the ExtendAnimation used to extend windows during animations -->
     <declare-styleable name="ExtendAnimation">
-        <!-- Defines the amount a window should be extended outward from the left at
-             the start of the animation. -->
-        <attr name="fromExtendLeft" format="float|fraction" />
-        <!-- Defines the amount a window should be extended outward from the top at
-             the start of the animation. -->
-        <attr name="fromExtendTop" format="float|fraction" />
-        <!-- Defines the amount a window should be extended outward from the right at
-             the start of the animation. -->
-        <attr name="fromExtendRight" format="float|fraction" />
-        <!-- Defines the amount a window should be extended outward from the bottom at
-             the start of the animation. -->
-        <attr name="fromExtendBottom" format="float|fraction" />
-        <!-- Defines the amount a window should be extended outward from the left by
-             the end of the animation by transitioning from the fromExtendLeft amount. -->
-        <attr name="toExtendLeft" format="float|fraction" />
-        <!-- Defines the amount a window should be extended outward from the top by
-             the end of the animation by transitioning from the fromExtendTop amount. -->
-        <attr name="toExtendTop" format="float|fraction" />
-        <!-- Defines the amount a window should be extended outward from the right by
-             the end of the animation by transitioning from the fromExtendRight amount. -->
-        <attr name="toExtendRight" format="float|fraction" />
-        <!-- Defines the amount a window should be extended outward from the bottom by
-             the end of the animation by transitioning from the fromExtendBottom amount. -->
-        <attr name="toExtendBottom" format="float|fraction" />
+        <!-- Defines the amount a window should be extended outward from the left at the start of
+             the animation in an absolute dimension (interpreted as pixels if no dimension unit is
+             provided) or as a percentage of the animation target's width. -->
+        <attr name="fromExtendLeft" format="float|fraction|dimension" />
+        <!-- Defines the amount a window should be extended outward from the top at the start of
+             the animation in an absolute dimension (interpreted as pixels if no dimension unit is
+             provided) or as a percentage of the animation target's height. -->
+        <attr name="fromExtendTop" format="float|fraction|dimension" />
+        <!-- Defines the amount a window should be extended outward from the right at the start of
+             the animation in an absolute dimension (interpreted as pixels if no dimension unit is
+             provided) or as a percentage of the animation target's width. -->
+        <attr name="fromExtendRight" format="float|fraction|dimension" />
+        <!-- Defines the amount a window should be extended outward from the bottom at the start of
+             the animation in an absolute dimension (interpreted as pixels if no dimension unit is
+             provided) or as a percentage of the animation target's height. -->
+        <attr name="fromExtendBottom" format="float|fraction|dimension" />
+        <!-- Defines the amount a window should be extended outward from the left by the end of the
+             animation by transitioning from the fromExtendLeft amount in an absolute dimension
+             (interpreted as pixels if no dimension unit is provided) or as a percentage of the
+             animation target's width. -->
+        <attr name="toExtendLeft" format="float|fraction|dimension" />
+        <!-- Defines the amount a window should be extended outward from the top by the end of the
+             animation by transitioning from the fromExtendTop amount in an absolute dimension
+             (interpreted as pixels if no dimension unit is provided) or as a percentage of the
+             animation target's height. -->
+        <attr name="toExtendTop" format="float|fraction|dimension" />
+        <!-- Defines the amount a window should be extended outward from the right by the end of
+             the animation by transitioning from the fromExtendRight amount in an absolute
+             dimension (interpreted as pixels if no dimension unit is provided) or as a percentage
+             of the animation target's width. -->
+        <attr name="toExtendRight" format="float|fraction|dimension" />
+        <!-- Defines the amount a window should be extended outward from the bottom by the end of
+             the animation by transitioning from the fromExtendBottom amount in an absolute
+             dimension (interpreted as pixels if no dimension unit is provided) or as a percentage
+             of the animation target's height. -->
+        <attr name="toExtendBottom" format="float|fraction|dimension" />
     </declare-styleable>
 
     <declare-styleable name="LayoutAnimation">
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 744c3dab..adf8f8e 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -606,6 +606,9 @@
     <!-- The padding ratio of the Accessibility icon foreground drawable -->
     <item name="accessibility_icon_foreground_padding_ratio" type="dimen">21.88%</item>
 
+    <!-- The minimum window size of the accessibility window magnifier -->
+    <dimen name="accessibility_window_magnifier_min_size">122dp</dimen>
+
     <!-- Margin around the various security views -->
     <dimen name="keyguard_muliuser_selector_margin">8dp</dimen>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e7eeecc..558e3c3 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1676,6 +1676,7 @@
   <java-symbol type="id" name="media_route_volume_slider" />
   <java-symbol type="id" name="media_route_control_frame" />
   <java-symbol type="id" name="media_route_extended_settings_button" />
+  <java-symbol type="id" name="media_route_progress_bar" />
   <java-symbol type="string" name="media_route_chooser_title" />
   <java-symbol type="string" name="media_route_chooser_title_for_remote_display" />
   <java-symbol type="string" name="media_route_controller_disconnect" />
@@ -4398,6 +4399,7 @@
   <java-symbol type="color" name="accessibility_focus_highlight_color" />
   <!-- Width of the outline stroke used by the accessibility focus rectangle -->
   <java-symbol type="dimen" name="accessibility_focus_highlight_stroke_width" />
+  <java-symbol type="dimen" name="accessibility_window_magnifier_min_size" />
 
   <java-symbol type="bool" name="config_attachNavBarToAppDuringTransition" />
 
diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java
index 2f978fc..582488f 100644
--- a/graphics/java/android/graphics/ColorSpace.java
+++ b/graphics/java/android/graphics/ColorSpace.java
@@ -22,6 +22,10 @@
 import android.annotation.Nullable;
 import android.annotation.Size;
 import android.annotation.SuppressAutoDoc;
+import android.annotation.SuppressLint;
+import android.hardware.DataSpace;
+import android.hardware.DataSpace.NamedDataSpace;
+import android.util.SparseIntArray;
 
 import libcore.util.NativeAllocationRegistry;
 
@@ -207,6 +211,7 @@
 
     // See static initialization block next to #get(Named)
     private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length];
+    private static final SparseIntArray sDataToColorSpaces = new SparseIntArray();
 
     @NonNull private final String mName;
     @NonNull private final Model mModel;
@@ -1389,6 +1394,47 @@
     }
 
     /**
+     * Create a {@link ColorSpace} object using a {@link android.hardware.DataSpace DataSpace}
+     * value.
+     *
+     * <p>This function maps from a dataspace to a {@link Named} ColorSpace.
+     * If no {@link Named} ColorSpace object matching the {@code dataSpace} value can be created,
+     * {@code null} will return.</p>
+     *
+     * @param dataSpace The dataspace value
+     * @return the ColorSpace object or {@code null} if no matching colorspace can be found.
+     */
+    @SuppressLint("MethodNameUnits")
+    @Nullable
+    public static ColorSpace getFromDataSpace(@NamedDataSpace int dataSpace) {
+        int index = sDataToColorSpaces.get(dataSpace, -1);
+        if (index != -1) {
+            return ColorSpace.get(index);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Retrieve the {@link android.hardware.DataSpace DataSpace} value from a {@link ColorSpace}
+     * object.
+     *
+     * <p>If this {@link ColorSpace} object has no matching {@code dataSpace} value,
+     * {@link android.hardware.DataSpace#DATASPACE_UNKNOWN DATASPACE_UNKNOWN} will return.</p>
+     *
+     * @return the dataspace value.
+     */
+    @SuppressLint("MethodNameUnits")
+    public @NamedDataSpace int getDataSpace() {
+        int index = sDataToColorSpaces.indexOfValue(getId());
+        if (index != -1) {
+            return sDataToColorSpaces.keyAt(index);
+        } else {
+            return DataSpace.DATASPACE_UNKNOWN;
+        }
+    }
+
+    /**
      * <p>Returns an instance of {@link ColorSpace} identified by the specified
      * name. The list of names provided in the {@link Named} enum gives access
      * to a variety of common RGB color spaces.</p>
@@ -1445,6 +1491,7 @@
                 SRGB_TRANSFER_PARAMETERS,
                 Named.SRGB.ordinal()
         );
+        sDataToColorSpaces.put(DataSpace.DATASPACE_SRGB, Named.SRGB.ordinal());
         sNamedColorSpaces[Named.LINEAR_SRGB.ordinal()] = new ColorSpace.Rgb(
                 "sRGB IEC61966-2.1 (Linear)",
                 SRGB_PRIMARIES,
@@ -1453,6 +1500,7 @@
                 0.0f, 1.0f,
                 Named.LINEAR_SRGB.ordinal()
         );
+        sDataToColorSpaces.put(DataSpace.DATASPACE_SRGB_LINEAR, Named.LINEAR_SRGB.ordinal());
         sNamedColorSpaces[Named.EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb(
                 "scRGB-nl IEC 61966-2-2:2003",
                 SRGB_PRIMARIES,
@@ -1464,6 +1512,7 @@
                 SRGB_TRANSFER_PARAMETERS,
                 Named.EXTENDED_SRGB.ordinal()
         );
+        sDataToColorSpaces.put(DataSpace.DATASPACE_SCRGB, Named.EXTENDED_SRGB.ordinal());
         sNamedColorSpaces[Named.LINEAR_EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb(
                 "scRGB IEC 61966-2-2:2003",
                 SRGB_PRIMARIES,
@@ -1472,6 +1521,8 @@
                 -0.5f, 7.499f,
                 Named.LINEAR_EXTENDED_SRGB.ordinal()
         );
+        sDataToColorSpaces.put(
+                DataSpace.DATASPACE_SCRGB_LINEAR, Named.LINEAR_EXTENDED_SRGB.ordinal());
         sNamedColorSpaces[Named.BT709.ordinal()] = new ColorSpace.Rgb(
                 "Rec. ITU-R BT.709-5",
                 new float[] { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f },
@@ -1480,6 +1531,7 @@
                 new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
                 Named.BT709.ordinal()
         );
+        sDataToColorSpaces.put(DataSpace.DATASPACE_BT709, Named.BT709.ordinal());
         sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb(
                 "Rec. ITU-R BT.2020-1",
                 new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f },
@@ -1488,6 +1540,7 @@
                 new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45),
                 Named.BT2020.ordinal()
         );
+        sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020, Named.BT2020.ordinal());
         sNamedColorSpaces[Named.DCI_P3.ordinal()] = new ColorSpace.Rgb(
                 "SMPTE RP 431-2-2007 DCI (P3)",
                 new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f },
@@ -1496,6 +1549,7 @@
                 0.0f, 1.0f,
                 Named.DCI_P3.ordinal()
         );
+        sDataToColorSpaces.put(DataSpace.DATASPACE_DCI_P3, Named.DCI_P3.ordinal());
         sNamedColorSpaces[Named.DISPLAY_P3.ordinal()] = new ColorSpace.Rgb(
                 "Display P3",
                 new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f },
@@ -1504,6 +1558,7 @@
                 SRGB_TRANSFER_PARAMETERS,
                 Named.DISPLAY_P3.ordinal()
         );
+        sDataToColorSpaces.put(DataSpace.DATASPACE_DISPLAY_P3, Named.DISPLAY_P3.ordinal());
         sNamedColorSpaces[Named.NTSC_1953.ordinal()] = new ColorSpace.Rgb(
                 "NTSC (1953)",
                 NTSC_1953_PRIMARIES,
@@ -1528,6 +1583,7 @@
                 0.0f, 1.0f,
                 Named.ADOBE_RGB.ordinal()
         );
+        sDataToColorSpaces.put(DataSpace.DATASPACE_ADOBE_RGB, Named.ADOBE_RGB.ordinal());
         sNamedColorSpaces[Named.PRO_PHOTO_RGB.ordinal()] = new ColorSpace.Rgb(
                 "ROMM RGB ISO 22028-2:2013",
                 new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f },
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index f7c92fe..8a482fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -144,21 +144,42 @@
         return Math.max(dividerInset, radius);
     }
 
-    /** Gets bounds of the primary split. */
+    /** Gets bounds of the primary split with screen based coordinate. */
     public Rect getBounds1() {
         return new Rect(mBounds1);
     }
 
-    /** Gets bounds of the secondary split. */
+    /** Gets bounds of the primary split with parent based coordinate. */
+    public Rect getRefBounds1() {
+        Rect outBounds = getBounds1();
+        outBounds.offset(-mRootBounds.left, -mRootBounds.top);
+        return outBounds;
+    }
+
+    /** Gets bounds of the secondary split with screen based coordinate. */
     public Rect getBounds2() {
         return new Rect(mBounds2);
     }
 
-    /** Gets bounds of divider window. */
+    /** Gets bounds of the secondary split with parent based coordinate. */
+    public Rect getRefBounds2() {
+        final Rect outBounds = getBounds2();
+        outBounds.offset(-mRootBounds.left, -mRootBounds.top);
+        return outBounds;
+    }
+
+    /** Gets bounds of divider window with screen based coordinate. */
     public Rect getDividerBounds() {
         return new Rect(mDividerBounds);
     }
 
+    /** Gets bounds of divider window with parent based coordinate. */
+    public Rect getRefDividerBounds() {
+        final Rect outBounds = getDividerBounds();
+        outBounds.offset(-mRootBounds.left, -mRootBounds.top);
+        return outBounds;
+    }
+
     /** Returns leash of the current divider bar. */
     @Nullable
     public SurfaceControl getDividerLeash() {
@@ -452,14 +473,17 @@
             SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
         final SurfaceControl dividerLeash = getDividerLeash();
         if (dividerLeash != null) {
-            t.setPosition(dividerLeash, mDividerBounds.left, mDividerBounds.top);
+            mTempRect.set(getRefDividerBounds());
+            t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
             // Resets layer of divider bar to make sure it is always on top.
             t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER);
         }
-        t.setPosition(leash1, mBounds1.left, mBounds1.top)
-                .setWindowCrop(leash1, mBounds1.width(), mBounds1.height());
-        t.setPosition(leash2, mBounds2.left, mBounds2.top)
-                .setWindowCrop(leash2, mBounds2.width(), mBounds2.height());
+        mTempRect.set(getRefBounds1());
+        t.setPosition(leash1, mTempRect.left, mTempRect.top)
+                .setWindowCrop(leash1, mTempRect.width(), mTempRect.height());
+        mTempRect.set(getRefBounds2());
+        t.setPosition(leash2, mTempRect.left, mTempRect.top)
+                .setWindowCrop(leash2, mTempRect.width(), mTempRect.height());
 
         if (mImePositionProcessor.adjustSurfaceLayoutForIme(
                 t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index 101295d..11ecc91 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -35,6 +35,9 @@
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.content.ClipDescription;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -54,6 +57,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.R;
+import com.android.wm.shell.animation.Interpolators;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -205,6 +209,7 @@
                 break;
             case ACTION_DRAG_ENTERED:
                 pd.dragLayout.show();
+                pd.dragLayout.update(event);
                 break;
             case ACTION_DRAG_LOCATION:
                 pd.dragLayout.update(event);
@@ -250,10 +255,6 @@
                 // Hide the window if another drag hasn't been started while animating the drop
                 setDropTargetWindowVisibility(pd, View.INVISIBLE);
             }
-
-            // Clean up the drag surface
-            mTransaction.reparent(dragSurface, null);
-            mTransaction.apply();
         });
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index e8bae0f..7568310 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -123,6 +123,13 @@
     }
 
     /**
+     * Returns the number of targets.
+     */
+    int getNumTargets() {
+        return mTargets.size();
+    }
+
+    /**
      * Returns the target's regions based on the current state of the device and display.
      */
     @NonNull
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index d395f95..25fe8b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.draganddrop;
 
 import static android.app.StatusBarManager.DISABLE_NONE;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -24,6 +25,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.StatusBarManager;
@@ -44,6 +46,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.R;
+import com.android.wm.shell.animation.Interpolators;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -135,6 +138,12 @@
         }
     }
 
+    private void updateContainerMarginsForSingleTask() {
+        mDropZoneView1.setContainerMargin(
+                mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
+        mDropZoneView2.setContainerMargin(0, 0, 0, 0);
+    }
+
     private void updateContainerMargins(int orientation) {
         final float halfMargin = mDisplayMargin / 2f;
         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
@@ -165,11 +174,20 @@
         if (!alreadyInSplit) {
             ActivityManager.RunningTaskInfo taskInfo1 = mPolicy.getLatestRunningTask();
             if (taskInfo1 != null) {
-                Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
-                int bgColor1 = getResizingBackgroundColor(taskInfo1);
-                mDropZoneView1.setAppInfo(bgColor1, icon1);
-                mDropZoneView2.setAppInfo(bgColor1, icon1);
-                updateDropZoneSizes(null, null); // passing null splits the views evenly
+                final int activityType = taskInfo1.getActivityType();
+                if (activityType == ACTIVITY_TYPE_STANDARD) {
+                    Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
+                    int bgColor1 = getResizingBackgroundColor(taskInfo1);
+                    mDropZoneView1.setAppInfo(bgColor1, icon1);
+                    mDropZoneView2.setAppInfo(bgColor1, icon1);
+                    updateDropZoneSizes(null, null); // passing null splits the views evenly
+                } else {
+                    // We use the first drop zone to show the fullscreen highlight, and don't need
+                    // to set additional info
+                    mDropZoneView1.setForceIgnoreBottomMargin(true);
+                    updateDropZoneSizesForSingleTask();
+                    updateContainerMarginsForSingleTask();
+                }
             }
         } else {
             // We're already in split so get taskInfo from the controller to populate icon / color.
@@ -195,6 +213,21 @@
         }
     }
 
+    private void updateDropZoneSizesForSingleTask() {
+        final LinearLayout.LayoutParams dropZoneView1 =
+                (LayoutParams) mDropZoneView1.getLayoutParams();
+        final LinearLayout.LayoutParams dropZoneView2 =
+                (LayoutParams) mDropZoneView2.getLayoutParams();
+        dropZoneView1.width = MATCH_PARENT;
+        dropZoneView1.height = MATCH_PARENT;
+        dropZoneView2.width = 0;
+        dropZoneView2.height = 0;
+        dropZoneView1.weight = 1;
+        dropZoneView2.weight = 0;
+        mDropZoneView1.setLayoutParams(dropZoneView1);
+        mDropZoneView2.setLayoutParams(dropZoneView2);
+    }
+
     /**
      * Sets the size of the two drop zones based on the provided bounds. The divider sits between
      * the views and its size is included in the calculations.
@@ -265,9 +298,12 @@
                 // Animating to no target
                 animateSplitContainers(false, null /* animCompleteCallback */);
             } else if (mCurrentTarget == null) {
-                // Animating to first target
-                animateSplitContainers(true, null /* animCompleteCallback */);
-                animateHighlight(target);
+                if (mPolicy.getNumTargets() == 1) {
+                    animateFullscreenContainer(true);
+                } else {
+                    animateSplitContainers(true, null /* animCompleteCallback */);
+                    animateHighlight(target);
+                }
             } else {
                 // Switching between targets
                 mDropZoneView1.animateSwitch();
@@ -283,6 +319,10 @@
     public void hide(DragEvent event, Runnable hideCompleteCallback) {
         mIsShowing = false;
         animateSplitContainers(false, hideCompleteCallback);
+        // Reset the state if we previously force-ignore the bottom margin
+        mDropZoneView1.setForceIgnoreBottomMargin(false);
+        mDropZoneView2.setForceIgnoreBottomMargin(false);
+        updateContainerMargins(getResources().getConfiguration().orientation);
         mCurrentTarget = null;
     }
 
@@ -297,11 +337,63 @@
         // Process the drop
         mPolicy.handleDrop(mCurrentTarget, event.getClipData());
 
-        // TODO(b/169894807): Coordinate with dragSurface
+        // Start animating the drop UI out with the drag surface
         hide(event, dropCompleteCallback);
+        hideDragSurface(dragSurface);
         return handledDrop;
     }
 
+    private void hideDragSurface(SurfaceControl dragSurface) {
+        final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+        final ValueAnimator dragSurfaceAnimator = ValueAnimator.ofFloat(0f, 1f);
+        // Currently the splash icon animation runs with the default ValueAnimator duration of
+        // 300ms
+        dragSurfaceAnimator.setDuration(300);
+        dragSurfaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        dragSurfaceAnimator.addUpdateListener(animation -> {
+            float t = animation.getAnimatedFraction();
+            float alpha = 1f - t;
+            // TODO: Scale the drag surface as well once we make all the source surfaces
+            //       consistent
+            tx.setAlpha(dragSurface, alpha);
+            tx.apply();
+        });
+        dragSurfaceAnimator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCanceled = false;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                cleanUpSurface();
+                mCanceled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCanceled) {
+                    // Already handled above
+                    return;
+                }
+                cleanUpSurface();
+            }
+
+            private void cleanUpSurface() {
+                // Clean up the drag surface
+                tx.remove(dragSurface);
+                tx.apply();
+            }
+        });
+        dragSurfaceAnimator.start();
+    }
+
+    private void animateFullscreenContainer(boolean visible) {
+        mStatusBarManager.disable(visible
+                ? HIDE_STATUS_BAR_FLAGS
+                : DISABLE_NONE);
+        // We're only using the first drop zone if there is one fullscreen target
+        mDropZoneView1.setShowingMargin(visible);
+        mDropZoneView1.setShowingHighlight(visible);
+    }
+
     private void animateSplitContainers(boolean visible, Runnable animCompleteCallback) {
         mStatusBarManager.disable(visible
                 ? HIDE_STATUS_BAR_FLAGS
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
index a3ee8ae..38870bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
@@ -65,6 +65,7 @@
     private final float[] mContainerMargin = new float[4];
     private float mCornerRadius;
     private float mBottomInset;
+    private boolean mIgnoreBottomMargin;
     private int mMarginColor; // i.e. color used for negative space like the container insets
 
     private boolean mShowingHighlight;
@@ -141,6 +142,14 @@
         }
     }
 
+    /** Ignores the bottom margin provided by the insets. */
+    public void setForceIgnoreBottomMargin(boolean ignoreBottomMargin) {
+        mIgnoreBottomMargin = ignoreBottomMargin;
+        if (mMarginPercent > 0) {
+            mMarginView.invalidate();
+        }
+    }
+
     /** Sets the bottom inset so the drop zones are above bottom navigation. */
     public void setBottomInset(float bottom) {
         mBottomInset = bottom;
@@ -257,7 +266,8 @@
             mPath.addRoundRect(mContainerMargin[0] * mMarginPercent,
                     mContainerMargin[1] * mMarginPercent,
                     getWidth() - (mContainerMargin[2] * mMarginPercent),
-                    getHeight() - (mContainerMargin[3] * mMarginPercent) - mBottomInset,
+                    getHeight() - (mContainerMargin[3] * mMarginPercent)
+                            - (mIgnoreBottomMargin ? 0 : mBottomInset),
                     mCornerRadius * mMarginPercent,
                     mCornerRadius * mMarginPercent,
                     Path.Direction.CW);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index 180e3fb..d7322ce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -138,8 +138,8 @@
         // destination are different.
         final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH;
         final Rect crop = mTmpDestinationRect;
-        crop.set(0, 0, Transitions.ENABLE_SHELL_TRANSITIONS ? destH
-                : destW, Transitions.ENABLE_SHELL_TRANSITIONS ? destW : destH);
+        crop.set(0, 0, Transitions.SHELL_TRANSITIONS_ROTATION ? destH
+                : destW, Transitions.SHELL_TRANSITIONS_ROTATION ? destW : destH);
         // Inverse scale for crop to fit in screen coordinates.
         crop.scale(1 / scale);
         crop.offset(insets.left, insets.top);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 60aac68..91615fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -97,7 +97,7 @@
      * meaningful if {@link #mInFixedRotation} is true.
      */
     @Surface.Rotation
-    private int mFixedRotation;
+    private int mEndFixedRotation;
     /** Whether the PIP window has fade out for fixed rotation. */
     private boolean mHasFadeOut;
 
@@ -153,7 +153,7 @@
         final TransitionInfo.Change currentPipChange = findCurrentPipChange(info);
         final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
         mInFixedRotation = fixedRotationChange != null;
-        mFixedRotation = mInFixedRotation
+        mEndFixedRotation = mInFixedRotation
                 ? fixedRotationChange.getEndFixedRotation()
                 : ROTATION_UNDEFINED;
 
@@ -257,7 +257,7 @@
         final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo();
         if (taskInfo != null) {
             startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(),
-                    new Rect(mExitDestinationBounds));
+                    new Rect(mExitDestinationBounds), Surface.ROTATION_0);
         }
         mExitDestinationBounds.setEmpty();
         mCurrentPipTaskToken = null;
@@ -332,30 +332,31 @@
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback,
             @NonNull TransitionInfo.Change pipChange) {
-        TransitionInfo.Change displayRotationChange = null;
-        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-            final TransitionInfo.Change change = info.getChanges().get(i);
-            if (change.getMode() == TRANSIT_CHANGE
-                    && (change.getFlags() & FLAG_IS_DISPLAY) != 0
-                    && change.getStartRotation() != change.getEndRotation()) {
-                displayRotationChange = change;
-                break;
-            }
-        }
-
-        if (displayRotationChange != null) {
-            // Exiting PIP to fullscreen with orientation change.
-            startExpandAndRotationAnimation(info, startTransaction, finishTransaction,
-                    finishCallback, displayRotationChange, pipChange);
-            return;
-        }
-
-        // When there is no rotation, we can simply expand the PIP window.
         mFinishCallback = (wct, wctCB) -> {
             mPipOrganizer.onExitPipFinished(pipChange.getTaskInfo());
             finishCallback.onTransitionFinished(wct, wctCB);
         };
 
+        // Check if it is Shell rotation.
+        if (Transitions.SHELL_TRANSITIONS_ROTATION) {
+            TransitionInfo.Change displayRotationChange = null;
+            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                final TransitionInfo.Change change = info.getChanges().get(i);
+                if (change.getMode() == TRANSIT_CHANGE
+                        && (change.getFlags() & FLAG_IS_DISPLAY) != 0
+                        && change.getStartRotation() != change.getEndRotation()) {
+                    displayRotationChange = change;
+                    break;
+                }
+            }
+            if (displayRotationChange != null) {
+                // Exiting PIP to fullscreen with orientation change.
+                startExpandAndRotationAnimation(info, startTransaction, finishTransaction,
+                        displayRotationChange, pipChange);
+                return;
+            }
+        }
+
         // Set the initial frame as scaling the end to the start.
         final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds());
         final Point offset = pipChange.getEndRelOffset();
@@ -364,13 +365,41 @@
         mSurfaceTransactionHelper.scale(startTransaction, pipChange.getLeash(),
                 destinationBounds, mPipBoundsState.getBounds());
         startTransaction.apply();
-        startExpandAnimation(pipChange.getTaskInfo(), pipChange.getLeash(), destinationBounds);
+
+        // Check if it is fixed rotation.
+        final int rotationDelta;
+        if (mInFixedRotation) {
+            final int startRotation = pipChange.getStartRotation();
+            final int endRotation = mEndFixedRotation;
+            rotationDelta = deltaRotation(startRotation, endRotation);
+            final Rect endBounds = new Rect(destinationBounds);
+
+            // Set the end frame since the display won't rotate until fixed rotation is finished
+            // in the next display change transition.
+            rotateBounds(endBounds, destinationBounds, rotationDelta);
+            final int degree, x, y;
+            if (rotationDelta == ROTATION_90) {
+                degree = 90;
+                x = destinationBounds.right;
+                y = destinationBounds.top;
+            } else {
+                degree = -90;
+                x = destinationBounds.left;
+                y = destinationBounds.bottom;
+            }
+            mSurfaceTransactionHelper.rotateAndScaleWithCrop(finishTransaction,
+                    pipChange.getLeash(), endBounds, endBounds, new Rect(), degree, x, y,
+                    true /* isExpanding */, rotationDelta == ROTATION_270 /* clockwise */);
+        } else {
+            rotationDelta = Surface.ROTATION_0;
+        }
+        startExpandAnimation(pipChange.getTaskInfo(), pipChange.getLeash(), destinationBounds,
+                rotationDelta);
     }
 
     private void startExpandAndRotationAnimation(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback,
             @NonNull TransitionInfo.Change displayRotationChange,
             @NonNull TransitionInfo.Change pipChange) {
         final int rotateDelta = deltaRotation(displayRotationChange.getStartRotation(),
@@ -380,11 +409,6 @@
         final CounterRotatorHelper rotator = new CounterRotatorHelper();
         rotator.handleClosingChanges(info, startTransaction, displayRotationChange);
 
-        mFinishCallback = (wct, wctCB) -> {
-            mPipOrganizer.onExitPipFinished(pipChange.getTaskInfo());
-            finishCallback.onTransitionFinished(wct, wctCB);
-        };
-
         // Get the start bounds in new orientation.
         final Rect startBounds = new Rect(pipChange.getStartAbsBounds());
         rotateBounds(startBounds, displayRotationChange.getStartAbsBounds(), rotateDelta);
@@ -425,12 +449,11 @@
     }
 
     private void startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash,
-            final Rect destinationBounds) {
-        PipAnimationController.PipTransitionAnimator animator =
+            final Rect destinationBounds, final int rotationDelta) {
+        final PipAnimationController.PipTransitionAnimator animator =
                 mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(),
                         mPipBoundsState.getBounds(), destinationBounds, null,
-                        TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, Surface.ROTATION_0);
-
+                        TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, rotationDelta);
         animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(mEnterExitAnimationDuration)
@@ -526,7 +549,7 @@
 
         mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP);
         mFinishCallback = finishCallback;
-        final int endRotation = mInFixedRotation ? mFixedRotation : enterPip.getEndRotation();
+        final int endRotation = mInFixedRotation ? mEndFixedRotation : enterPip.getEndRotation();
         return startEnterAnimation(enterPip.getTaskInfo(), enterPip.getLeash(),
                 startTransaction, finishTransaction, enterPip.getStartRotation(),
                 endRotation);
@@ -545,8 +568,8 @@
                 taskInfo.pictureInPictureParams, currentBounds);
         if (rotationDelta != Surface.ROTATION_0 && mInFixedRotation) {
             // Need to get the bounds of new rotation in old rotation for fixed rotation,
-            sourceHintRect = computeRotatedBounds(rotationDelta, startRotation, endRotation,
-                    taskInfo, TRANSITION_DIRECTION_TO_PIP, destinationBounds, sourceHintRect);
+            computeEnterPipRotatedBounds(rotationDelta, startRotation, endRotation, taskInfo,
+                    destinationBounds, sourceHintRect);
         }
         PipAnimationController.PipTransitionAnimator animator;
         // Set corner radius for entering pip.
@@ -583,8 +606,6 @@
             startTransaction.setMatrix(leash, tmpTransform, new float[9]);
         }
         if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
-            // Reverse the rotation for Shell transition animation.
-            rotationDelta = deltaRotation(rotationDelta, 0);
             animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds,
                     currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
                     0 /* startingAngle */, rotationDelta);
@@ -617,33 +638,22 @@
         return true;
     }
 
-    /** Computes destination bounds in old rotation and returns source hint rect if available. */
-    @Nullable
-    private Rect computeRotatedBounds(int rotationDelta, int startRotation, int endRotation,
-            TaskInfo taskInfo, int direction, Rect outDestinationBounds,
-            @Nullable Rect sourceHintRect) {
-        if (direction == TRANSITION_DIRECTION_TO_PIP) {
-            mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), endRotation);
-            final Rect displayBounds = mPipBoundsState.getDisplayBounds();
-            outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
-            // Transform the destination bounds to current display coordinates.
-            rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation);
-            // When entering PiP (from button navigation mode), adjust the source rect hint by
-            // display cutout if applicable.
-            if (sourceHintRect != null && taskInfo.displayCutoutInsets != null) {
-                if (rotationDelta == Surface.ROTATION_270) {
-                    sourceHintRect.offset(taskInfo.displayCutoutInsets.left,
-                            taskInfo.displayCutoutInsets.top);
-                }
+    /** Computes destination bounds in old rotation and updates source hint rect if available. */
+    private void computeEnterPipRotatedBounds(int rotationDelta, int startRotation, int endRotation,
+            TaskInfo taskInfo, Rect outDestinationBounds, @Nullable Rect outSourceHintRect) {
+        mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), endRotation);
+        final Rect displayBounds = mPipBoundsState.getDisplayBounds();
+        outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
+        // Transform the destination bounds to current display coordinates.
+        rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation);
+        // When entering PiP (from button navigation mode), adjust the source rect hint by
+        // display cutout if applicable.
+        if (outSourceHintRect != null && taskInfo.displayCutoutInsets != null) {
+            if (rotationDelta == Surface.ROTATION_270) {
+                outSourceHintRect.offset(taskInfo.displayCutoutInsets.left,
+                        taskInfo.displayCutoutInsets.top);
             }
-        } else if (direction == TRANSITION_DIRECTION_LEAVE_PIP) {
-            final Rect rotatedDestinationBounds = new Rect(outDestinationBounds);
-            rotateBounds(rotatedDestinationBounds, mPipBoundsState.getDisplayBounds(),
-                    rotationDelta);
-            return PipBoundsAlgorithm.getValidSourceHintRect(taskInfo.pictureInPictureParams,
-                    rotatedDestinationBounds);
         }
-        return sourceHintRect;
     }
 
     private void startExitToSplitAnimation(TransitionInfo info,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 5996acd..5069180 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
@@ -289,7 +290,6 @@
         if (DEBUG) Log.d(TAG, "checkIfPinnedTaskAppeared(), task=" + pinnedTask);
         if (pinnedTask == null || pinnedTask.topActivity == null) return;
         mPinnedTaskId = pinnedTask.taskId;
-        setState(STATE_PIP);
 
         mPipMediaController.onActivityPinned();
         mPipNotificationController.show(pinnedTask.topActivity.getPackageName());
@@ -326,6 +326,9 @@
 
     @Override
     public void onPipTransitionFinished(int direction) {
+        if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) {
+            setState(STATE_PIP);
+        }
         if (DEBUG) Log.d(TAG, "onPipTransition_Finished(), state=" + stateToName(mState));
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 81dacdb..76641f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -979,7 +979,8 @@
             t.setAlpha(dividerLeash, 1);
             t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER);
             t.setPosition(dividerLeash,
-                    mSplitLayout.getDividerBounds().left, mSplitLayout.getDividerBounds().top);
+                    mSplitLayout.getRefDividerBounds().left,
+                    mSplitLayout.getRefDividerBounds().top);
         } else {
             t.hide(dividerLeash);
         }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
index c22d3f6..0b4bc76 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt
@@ -93,15 +93,4 @@
                 .isVisible(LAUNCHER_COMPONENT)
         }
     }
-
-    /**
-     * Checks that the focus doesn't change between windows during the transition
-     */
-    @Presubmit
-    @Test
-    open fun focusDoesNotChange() {
-        testSpec.assertEventLog {
-            this.focusDoesNotChange()
-        }
-    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
index 5214daa0..9c095a2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.flicker.pip
 
+import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -72,10 +73,17 @@
     @Test
     override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
 
-    /** {@inheritDoc}  */
-    @FlakyTest(bugId = 215869110)
+    /**
+     * Checks that the focus changes between the pip menu window and the launcher when clicking the
+     * dismiss button on pip menu to close the pip window.
+     */
+    @Presubmit
     @Test
-    override fun focusDoesNotChange() = super.focusDoesNotChange()
+    fun focusDoesNotChange() {
+        testSpec.assertEventLog {
+            this.focusChanges("PipMenuView", "NexusLauncherActivity")
+        }
+    }
 
     companion object {
         /**
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
index 332bba6a..ab07ede 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.flicker.pip
 
+import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -81,6 +82,17 @@
     @Test
     override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales()
 
+    /**
+     * Checks that the focus doesn't change between windows during the transition
+     */
+    @Presubmit
+    @Test
+    fun focusDoesNotChange() {
+        testSpec.assertEventLog {
+            this.focusDoesNotChange()
+        }
+    }
+
     companion object {
         /**
          * Creates the test configurations.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index dda1a82..85527c8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -18,6 +18,7 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
+
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 
@@ -51,6 +52,7 @@
         final SurfaceControl leash = createMockSurface();
         SplitLayout out = mock(SplitLayout.class);
         doReturn(dividerBounds).when(out).getDividerBounds();
+        doReturn(dividerBounds).when(out).getRefDividerBounds();
         doReturn(leash).when(out).getDividerLeash();
         return out;
     }
diff --git a/packages/CompanionDeviceManager/Android.bp b/packages/CompanionDeviceManager/Android.bp
index 6ded1637..9f5bfd4 100644
--- a/packages/CompanionDeviceManager/Android.bp
+++ b/packages/CompanionDeviceManager/Android.bp
@@ -34,6 +34,7 @@
 android_app {
     name: "CompanionDeviceManager",
     defaults: ["platform_app_defaults"],
+    certificate: "platform",
     srcs: ["src/**/*.java"],
 
     static_libs: [
diff --git a/packages/CompanionDeviceManager/AndroidManifest.xml b/packages/CompanionDeviceManager/AndroidManifest.xml
index 06f2d9d..8b5d214 100644
--- a/packages/CompanionDeviceManager/AndroidManifest.xml
+++ b/packages/CompanionDeviceManager/AndroidManifest.xml
@@ -31,6 +31,7 @@
     <uses-permission android:name="android.permission.RADIO_SCAN_WITHOUT_LOCATION"/>
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
     <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
 
     <application
         android:allowClearUserData="true"
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index b51d310..a6a8fcf 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -37,6 +37,7 @@
 import android.companion.CompanionDeviceManager;
 import android.companion.IAssociationRequestCallback;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.net.MacAddress;
 import android.os.Bundle;
 import android.os.Handler;
@@ -71,6 +72,9 @@
     private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
     private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
 
+    // Activity result: Internal Error.
+    private static final int RESULT_INTERNAL_ERROR = 2;
+
     // AssociationRequestsProcessor -> UI
     private static final int RESULT_CODE_ASSOCIATION_CREATED = 0;
     private static final String EXTRA_ASSOCIATION = "association";
@@ -191,6 +195,20 @@
     private void initUI() {
         if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest);
 
+        final String packageName = mRequest.getPackageName();
+        final int userId = mRequest.getUserId();
+        final CharSequence appLabel;
+
+        try {
+            appLabel = getApplicationLabel(this, packageName, userId);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Package u" + userId + "/" + packageName + " not found.");
+
+            CompanionDeviceDiscoveryService.stop(this);
+            setResultAndFinish(null, RESULT_INTERNAL_ERROR);
+            return;
+        }
+
         setContentView(R.layout.activity_confirmation);
 
         mTitle = findViewById(R.id.title);
@@ -203,8 +221,6 @@
         mButtonAllow.setOnClickListener(this::onPositiveButtonClick);
         findViewById(R.id.btn_negative).setOnClickListener(this::onNegativeButtonClick);
 
-        final CharSequence appLabel = getApplicationLabel(this, mRequest.getPackageName());
-
         if (mRequest.isSelfManaged()) {
             initUiForSelfManagedAssociation(appLabel);
         } else if (mRequest.isSingleDevice()) {
@@ -257,7 +273,7 @@
         if (DEBUG) Log.i(TAG, "onAssociationCreated(), association=" + association);
 
         // Don't need to notify the app, CdmService has already done that. Just finish.
-        setResultAndFinish(association);
+        setResultAndFinish(association, RESULT_OK);
     }
 
     private void cancel(boolean discoveryTimeout) {
@@ -284,10 +300,10 @@
         }
 
         // ... then set result and finish ("sending" onActivityResult()).
-        setResultAndFinish(null);
+        setResultAndFinish(null, RESULT_CANCELED);
     }
 
-    private void setResultAndFinish(@Nullable AssociationInfo association) {
+    private void setResultAndFinish(@Nullable AssociationInfo association, int resultCode) {
         if (DEBUG) Log.i(TAG, "setResultAndFinish(), association=" + association);
 
         final Intent data = new Intent();
@@ -297,7 +313,7 @@
                 data.putExtra(CompanionDeviceManager.EXTRA_DEVICE, mSelectedDevice.getDevice());
             }
         }
-        setResult(association != null ? RESULT_OK : RESULT_CANCELED, data);
+        setResult(resultCode, data);
 
         finish();
     }
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
index eab421e..e3e563d 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
@@ -50,14 +50,13 @@
     }
 
     static @NonNull CharSequence getApplicationLabel(
-            @NonNull Context context, @NonNull String packageName) {
+            @NonNull Context context, @NonNull String packageName, int userId)
+            throws PackageManager.NameNotFoundException {
         final PackageManager packageManager = context.getPackageManager();
-        final ApplicationInfo appInfo;
-        try {
-            appInfo = packageManager.getApplicationInfo(packageName, 0);
-        } catch (PackageManager.NameNotFoundException e) {
-            throw new RuntimeException(e);
-        }
+
+        final ApplicationInfo appInfo = packageManager.getApplicationInfoAsUser(
+                packageName, PackageManager.ApplicationInfoFlags.of(0), userId);
+
         return packageManager.getApplicationLabel(appInfo);
     }
 
diff --git a/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java
index 9175809..f681ba1 100644
--- a/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java
+++ b/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java
@@ -28,6 +28,7 @@
 import android.os.Parcelable;
 import android.os.Process;
 import android.os.SystemClock;
+import android.text.TextUtils;
 import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -500,7 +501,7 @@
                         && roaming == e.roaming && defaultNetwork == e.defaultNetwork
                         && rxBytes == e.rxBytes && rxPackets == e.rxPackets
                         && txBytes == e.txBytes && txPackets == e.txPackets
-                        && operations == e.operations && iface.equals(e.iface);
+                        && operations == e.operations && TextUtils.equals(iface, e.iface);
             }
             return false;
         }
diff --git a/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsService.java b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsService.java
index ef9ebb5..ef6f39a 100644
--- a/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsService.java
+++ b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsService.java
@@ -1887,7 +1887,13 @@
     private void deleteKernelTagData(int uid) {
         try {
             mCookieTagMap.forEach((key, value) -> {
-                if (value.uid == uid) {
+                // If SkDestroyListener deletes the socket tag while this code is running,
+                // forEach will either restart iteration from the beginning or return null,
+                // depending on when the deletion happens.
+                // If it returns null, continue iteration to delete the data and in fact it would
+                // just iterate from first key because BpfMap#getNextKey would return first key
+                // if the current key is not exist.
+                if (value != null && value.uid == uid) {
                     try {
                         mCookieTagMap.deleteEntry(key);
                     } catch (ErrnoException e) {
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java
index 72230b4..4117d0f 100644
--- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java
+++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java
@@ -177,7 +177,7 @@
                 ret = 0;
                 break;
             case SparseChunk.FILL:
-                ret = mCur.fill[(4 - ((int) mLeft & 0x3)) & 0x3];
+                ret = Byte.toUnsignedInt(mCur.fill[(4 - ((int) mLeft & 0x3)) & 0x3]);
                 break;
             default:
                 throw new IOException("Unsupported Chunk:" + mCur.toString());
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 8965144e..014a033 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1160,6 +1160,8 @@
     <string name="battery_info_status_not_charging">Connected, not charging</string>
     <!-- Battery Info screen. Value for a status item.  Used for diagnostic info screens, precise translation isn't needed -->
     <string name="battery_info_status_full">Charged</string>
+    <!-- [CHAR_LIMIT=40] Battery Info screen. Value for a status item. A state which device is fully charged -->
+    <string name="battery_info_status_full_charged">Fully Charged</string>
 
     <!-- Summary for settings preference disabled by administrator [CHAR LIMIT=50] -->
     <string name="disabled_by_admin_summary_text">Controlled by admin</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index f6e3557..19114cf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -194,9 +194,11 @@
      * @param context the context
      * @param batteryChangedIntent battery broadcast intent received from {@link
      *                             Intent.ACTION_BATTERY_CHANGED}.
+     * @param compactStatus to present compact battery charging string if {@code true}
      * @return battery status string
      */
-    public static String getBatteryStatus(Context context, Intent batteryChangedIntent) {
+    public static String getBatteryStatus(Context context, Intent batteryChangedIntent,
+            boolean compactStatus) {
         final int status = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_STATUS,
                 BatteryManager.BATTERY_STATUS_UNKNOWN);
         final Resources res = context.getResources();
@@ -205,10 +207,14 @@
         final BatteryStatus batteryStatus = new BatteryStatus(batteryChangedIntent);
 
         if (batteryStatus.isCharged()) {
-            statusString = res.getString(R.string.battery_info_status_full);
+            statusString = res.getString(compactStatus
+                    ? R.string.battery_info_status_full_charged
+                    : R.string.battery_info_status_full);
         } else {
             if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
-                if (batteryStatus.isPluggedInWired()) {
+                if (compactStatus) {
+                    statusString = res.getString(R.string.battery_info_status_charging);
+                } else if (batteryStatus.isPluggedInWired()) {
                     switch (batteryStatus.getChargingSpeed(context)) {
                         case BatteryStatus.CHARGING_FAST:
                             statusString = res.getString(
diff --git a/packages/SettingsLib/src/com/android/settingslib/devicestate/AndroidSecureSettings.java b/packages/SettingsLib/src/com/android/settingslib/devicestate/AndroidSecureSettings.java
new file mode 100644
index 0000000..8aee576
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/devicestate/AndroidSecureSettings.java
@@ -0,0 +1,54 @@
+/*
+ * 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.devicestate;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.provider.Settings;
+
+/**
+ * Implementation of {@link SecureSettings} that uses Android's {@link Settings.Secure}
+ * implementation.
+ */
+class AndroidSecureSettings implements SecureSettings {
+
+    private final ContentResolver mContentResolver;
+
+    AndroidSecureSettings(ContentResolver contentResolver) {
+        mContentResolver = contentResolver;
+    }
+
+    @Override
+    public void putStringForUser(String name, String value, int userHandle) {
+        Settings.Secure.putStringForUser(mContentResolver, name, value, userHandle);
+    }
+
+    @Override
+    public String getStringForUser(String name, int userHandle) {
+        return Settings.Secure.getStringForUser(mContentResolver, name, userHandle);
+    }
+
+    @Override
+    public void registerContentObserver(String name, boolean notifyForDescendants,
+            ContentObserver observer, int userHandle) {
+        mContentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(name),
+                notifyForDescendants,
+                observer,
+                userHandle);
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManager.java b/packages/SettingsLib/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManager.java
index afd3626..961fab3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManager.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.database.ContentObserver;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -47,28 +48,33 @@
 
     private static DeviceStateRotationLockSettingsManager sSingleton;
 
-    private final ContentResolver mContentResolver;
     private final String[] mDeviceStateRotationLockDefaults;
-    private final Handler mMainHandler = Handler.getMain();
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
     private final Set<DeviceStateRotationLockSettingsListener> mListeners = new HashSet<>();
+    private final SecureSettings mSecureSettings;
     private SparseIntArray mDeviceStateRotationLockSettings;
     private SparseIntArray mDeviceStateRotationLockFallbackSettings;
+    private String mLastSettingValue;
 
-    private DeviceStateRotationLockSettingsManager(Context context) {
-        mContentResolver = context.getContentResolver();
+    @VisibleForTesting
+    DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) {
+        this.mSecureSettings = secureSettings;
         mDeviceStateRotationLockDefaults =
                 context.getResources()
                         .getStringArray(R.array.config_perDeviceStateRotationLockDefaults);
         loadDefaults();
         initializeInMemoryMap();
-        listenForSettingsChange(context);
+        listenForSettingsChange();
     }
 
     /** Returns a singleton instance of this class */
     public static synchronized DeviceStateRotationLockSettingsManager getInstance(Context context) {
         if (sSingleton == null) {
+            Context applicationContext = context.getApplicationContext();
+            ContentResolver contentResolver = applicationContext.getContentResolver();
+            SecureSettings secureSettings = new AndroidSecureSettings(contentResolver);
             sSingleton =
-                    new DeviceStateRotationLockSettingsManager(context.getApplicationContext());
+                    new DeviceStateRotationLockSettingsManager(applicationContext, secureSettings);
         }
         return sSingleton;
     }
@@ -81,11 +87,11 @@
                 > 0;
     }
 
-    private void listenForSettingsChange(Context context) {
-        context.getContentResolver()
+    private void listenForSettingsChange() {
+        mSecureSettings
                 .registerContentObserver(
-                        Settings.Secure.getUriFor(Settings.Secure.DEVICE_STATE_ROTATION_LOCK),
-                        /* notifyForDescendents= */ false, //NOTYPO
+                        Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                        /* notifyForDescendants= */ false,
                         new ContentObserver(mMainHandler) {
                             @Override
                             public void onChange(boolean selfChange) {
@@ -182,8 +188,7 @@
 
     private void initializeInMemoryMap() {
         String serializedSetting =
-                Settings.Secure.getStringForUser(
-                        mContentResolver,
+                mSecureSettings.getStringForUser(
                         Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
                         UserHandle.USER_CURRENT);
         if (TextUtils.isEmpty(serializedSetting)) {
@@ -222,11 +227,7 @@
 
     private void persistSettings() {
         if (mDeviceStateRotationLockSettings.size() == 0) {
-            Settings.Secure.putStringForUser(
-                    mContentResolver,
-                    Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
-                    /* value= */ "",
-                    UserHandle.USER_CURRENT);
+            persistSettingIfChanged(/* newSettingValue= */ "");
             return;
         }
 
@@ -243,10 +244,17 @@
                     .append(SEPARATOR_REGEX)
                     .append(mDeviceStateRotationLockSettings.valueAt(i));
         }
-        Settings.Secure.putStringForUser(
-                mContentResolver,
+        persistSettingIfChanged(stringBuilder.toString());
+    }
+
+    private void persistSettingIfChanged(String newSettingValue) {
+        if (TextUtils.equals(mLastSettingValue, newSettingValue)) {
+            return;
+        }
+        mLastSettingValue = newSettingValue;
+        mSecureSettings.putStringForUser(
                 Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
-                stringBuilder.toString(),
+                /* value= */ newSettingValue,
                 UserHandle.USER_CURRENT);
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/devicestate/SecureSettings.java b/packages/SettingsLib/src/com/android/settingslib/devicestate/SecureSettings.java
new file mode 100644
index 0000000..1052873
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/devicestate/SecureSettings.java
@@ -0,0 +1,30 @@
+/*
+ * 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.devicestate;
+
+import android.database.ContentObserver;
+
+/** Minimal wrapper interface around {@link android.provider.Settings.Secure} for easier testing. */
+interface SecureSettings {
+
+    void putStringForUser(String name, String value, int userHandle);
+
+    String getStringForUser(String name, int userHandle);
+
+    void registerContentObserver(String name, boolean notifyForDescendants,
+            ContentObserver settingsObserver, int userHandle);
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java
index 93be66a..1e1dfae 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java
@@ -81,6 +81,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        setTheme(R.style.SudThemeGlifV3_DayNight);
         ThemeHelper.trySetDynamicColor(this);
         setContentView(R.layout.avatar_picker);
         setUpButtons();
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java
new file mode 100644
index 0000000..1a45384
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.devicestate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DeviceStateRotationLockSettingsManagerTest {
+
+    @Mock private Context mMockContext;
+    @Mock private Resources mMockResources;
+
+    private DeviceStateRotationLockSettingsManager mManager;
+    private int mNumSettingsChanges = 0;
+    private final ContentObserver mContentObserver = new ContentObserver(null) {
+        @Override
+        public void onChange(boolean selfChange) {
+            mNumSettingsChanges++;
+        }
+    };
+    private final FakeSecureSettings mFakeSecureSettings = new FakeSecureSettings();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        Context context = InstrumentationRegistry.getTargetContext();
+        when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
+        when(mMockContext.getResources()).thenReturn(mMockResources);
+        when(mMockContext.getContentResolver()).thenReturn(context.getContentResolver());
+        mFakeSecureSettings.registerContentObserver(
+                Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
+                /* notifyForDescendents= */ false, //NOTYPO
+                mContentObserver,
+                UserHandle.USER_CURRENT);
+        mManager = new DeviceStateRotationLockSettingsManager(context, mFakeSecureSettings);
+    }
+
+    @Test
+    public void initialization_settingsAreChangedOnce() {
+        assertThat(mNumSettingsChanges).isEqualTo(1);
+    }
+
+    @Test
+    public void updateSetting_multipleTimes_sameValue_settingsAreChangedOnlyOnce() {
+        mNumSettingsChanges = 0;
+
+        mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true);
+        mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true);
+        mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true);
+
+        assertThat(mNumSettingsChanges).isEqualTo(1);
+    }
+
+    @Test
+    public void updateSetting_multipleTimes_differentValues_settingsAreChangedMultipleTimes() {
+        mNumSettingsChanges = 0;
+
+        mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true);
+        mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ false);
+        mManager.updateSetting(/* deviceState= */ 1, /* rotationLocked= */ true);
+
+        assertThat(mNumSettingsChanges).isEqualTo(3);
+    }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/FakeSecureSettings.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/FakeSecureSettings.java
new file mode 100644
index 0000000..91baa68
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/FakeSecureSettings.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.devicestate;
+
+import android.database.ContentObserver;
+import android.util.Pair;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Fake implementation of {@link SecureSettings} that stores everything in memory. */
+class FakeSecureSettings implements SecureSettings {
+
+    private final Map<SettingsKey, String> mValues = new HashMap<>();
+    private final Multimap<SettingsKey, ContentObserver> mContentObservers = HashMultimap.create();
+
+    @Override
+    public void putStringForUser(String name, String value, int userHandle) {
+        SettingsKey settingsKey = new SettingsKey(userHandle, name);
+        mValues.put(settingsKey, value);
+        for (ContentObserver observer : mContentObservers.get(settingsKey)) {
+            observer.onChange(/* selfChange= */ false);
+        }
+    }
+
+    @Override
+    public String getStringForUser(String name, int userHandle) {
+        return mValues.getOrDefault(new SettingsKey(userHandle, name), "");
+    }
+
+    @Override
+    public void registerContentObserver(String name, boolean notifyForDescendants,
+            ContentObserver settingsObserver, int userHandle) {
+        mContentObservers.put(new SettingsKey(userHandle, name), settingsObserver);
+    }
+
+    private static class SettingsKey extends Pair<Integer, String> {
+
+        SettingsKey(Integer userHandle, String settingName) {
+            super(userHandle, settingName);
+        }
+    }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/OWNERS b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/OWNERS
new file mode 100644
index 0000000..98f4123
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/OWNERS
@@ -0,0 +1,3 @@
+# Default reviewers for this and subdirectories.
+alexflo@google.com
+chrisgollner@google.com
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
index 62de66e..09b2a2e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
@@ -320,28 +320,47 @@
         final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_LEVEL, 100);
         final Resources resources = mContext.getResources();
 
-        assertThat(Utils.getBatteryStatus(mContext, intent)).isEqualTo(
+        assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
                 resources.getString(R.string.battery_info_status_full));
     }
 
     @Test
+    public void getBatteryStatus_statusIsFullAndUseCompactStatus_returnFullyChargedString() {
+        final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_LEVEL, 100);
+        final Resources resources = mContext.getResources();
+
+        assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
+                resources.getString(R.string.battery_info_status_full_charged));
+    }
+
+    @Test
     public void getBatteryStatus_batteryLevelIs100_returnFullString() {
         final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_STATUS,
                 BatteryManager.BATTERY_STATUS_FULL);
         final Resources resources = mContext.getResources();
 
-        assertThat(Utils.getBatteryStatus(mContext, intent)).isEqualTo(
+        assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
                 resources.getString(R.string.battery_info_status_full));
     }
 
     @Test
+    public void getBatteryStatus_batteryLevelIs100AndUseCompactStatus_returnFullyString() {
+        final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_STATUS,
+                BatteryManager.BATTERY_STATUS_FULL);
+        final Resources resources = mContext.getResources();
+
+        assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
+                resources.getString(R.string.battery_info_status_full_charged));
+    }
+
+    @Test
     public void getBatteryStatus_batteryLevel99_returnChargingString() {
         final Intent intent = new Intent();
         intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING);
         intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB);
         final Resources resources = mContext.getResources();
 
-        assertThat(Utils.getBatteryStatus(mContext, intent)).isEqualTo(
+        assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
                 resources.getString(R.string.battery_info_status_charging));
     }
 
@@ -352,7 +371,29 @@
         intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_WIRELESS);
         final Resources resources = mContext.getResources();
 
-        assertThat(Utils.getBatteryStatus(mContext, intent)).isEqualTo(
+        assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
                 resources.getString(R.string.battery_info_status_charging_wireless));
     }
+
+    @Test
+    public void getBatteryStatus_chargingAndUseCompactStatus_returnCompactString() {
+        final Intent intent = new Intent();
+        intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING);
+        intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB);
+        final Resources resources = mContext.getResources();
+
+        assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
+                resources.getString(R.string.battery_info_status_charging));
+    }
+
+    @Test
+    public void getBatteryStatus_chargingWirelessAndUseCompactStatus_returnCompactString() {
+        final Intent intent = new Intent();
+        intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING);
+        intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_WIRELESS);
+        final Resources resources = mContext.getResources();
+
+        assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
+                resources.getString(R.string.battery_info_status_charging));
+    }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index c3f6a5d..0da60f0 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -298,6 +298,9 @@
              *
              * Important: The view must be attached to a [ViewGroup] when calling this function and
              * during the animation. For safety, this method will return null when it is not.
+             *
+             * Note: The background of [view] should be a (rounded) rectangle so that it can be
+             * properly animated.
              */
             @JvmStatic
             fun fromView(view: View, cujType: Int? = null): Controller? {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index a3c5649..50178f4 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -79,6 +79,9 @@
      * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be
      * animated when the dialog bounds change.
      *
+     * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
+     * animated.
+     *
      * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
      * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
      */
@@ -153,6 +156,9 @@
      * activity started, when the dialog to app animation is done (or when it is cancelled). If this
      * method returns null, then the dialog won't be dismissed.
      *
+     * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
+     * animated.
+     *
      * @param view any view inside the dialog to animate.
      */
     @JvmOverloads
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml
new file mode 100644
index 0000000..0544b871
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <solid android:color="@android:color/white"/>
+            <corners android:radius="18dp"/>
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="18dp"/>
+            <solid android:color="?androidprv:attr/colorAccentPrimary"/>
+        </shape>
+    </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
index d057f5f..31a8c3b 100644
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
+++ b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
@@ -19,13 +19,17 @@
     <ripple
         android:color="?android:attr/colorControlHighlight">
         <item android:id="@android:id/mask">
-            <shape android:shape="oval">
+            <!-- We make this shape a rounded rectangle instead of a oval so that it can animate -->
+            <!-- properly into an app/dialog. -->
+            <shape android:shape="rectangle">
                 <solid android:color="@android:color/white"/>
+                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
             </shape>
         </item>
         <item>
-            <shape android:shape="oval">
+            <shape android:shape="rectangle">
                 <solid android:color="?attr/offStateColor"/>
+                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
             </shape>
         </item>
 
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
index 944061c..021a85f 100644
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
+++ b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
@@ -19,13 +19,17 @@
     <ripple
         android:color="?android:attr/colorControlHighlight">
         <item android:id="@android:id/mask">
-            <shape android:shape="oval">
+            <!-- We make this shape a rounded rectangle instead of a oval so that it can animate -->
+            <!-- properly into an app/dialog. -->
+            <shape android:shape="rectangle">
                 <solid android:color="@android:color/white"/>
+                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
             </shape>
         </item>
         <item>
-            <shape android:shape="oval">
+            <shape android:shape="rectangle">
                 <solid android:color="?android:attr/colorAccent"/>
+                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
             </shape>
         </item>
 
diff --git a/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml b/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml
index a3e289a..e06bfdc 100644
--- a/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml
+++ b/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml
@@ -22,10 +22,6 @@
     android:scrollbarAlwaysDrawVerticalTrack="true"
     android:scrollIndicators="top|bottom"
     android:fillViewport="true"
-    android:paddingTop="@dimen/dialog_button_bar_top_padding"
-    android:paddingStart="@dimen/dialog_side_padding"
-    android:paddingEnd="@dimen/dialog_side_padding"
-    android:paddingBottom="@dimen/dialog_bottom_padding"
     style="?android:attr/buttonBarStyle">
     <com.android.internal.widget.ButtonBarLayout
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/alert_dialog_systemui.xml b/packages/SystemUI/res/layout/alert_dialog_systemui.xml
index f280cbd..ca8fadd 100644
--- a/packages/SystemUI/res/layout/alert_dialog_systemui.xml
+++ b/packages/SystemUI/res/layout/alert_dialog_systemui.xml
@@ -83,9 +83,15 @@
             android:layout_height="wrap_content" />
     </FrameLayout>
 
-    <include
+    <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        layout="@layout/alert_dialog_button_bar_systemui" />
+        android:paddingStart="@dimen/dialog_side_padding"
+        android:paddingEnd="@dimen/dialog_side_padding">
+        <include
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            layout="@layout/alert_dialog_button_bar_systemui" />
+    </FrameLayout>
 
 </com.android.internal.widget.AlertDialogLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/dream_overlay_container.xml b/packages/SystemUI/res/layout/dream_overlay_container.xml
index 330f515..8e83b4a 100644
--- a/packages/SystemUI/res/layout/dream_overlay_container.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_container.xml
@@ -34,34 +34,5 @@
         app:layout_constraintBottom_toBottomOf="parent"
         />
 
-    <com.android.systemui.dreams.DreamOverlayStatusBarView
-        android:id="@+id/dream_overlay_status_bar"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/dream_overlay_status_bar_height"
-        android:paddingEnd="@dimen/dream_overlay_status_bar_margin"
-        android:paddingStart="@dimen/dream_overlay_status_bar_margin"
-        app:layout_constraintTop_toTopOf="parent">
-
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:id="@+id/dream_overlay_system_status"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            app:layout_constraintEnd_toEndOf="parent">
-
-            <com.android.systemui.statusbar.AlphaOptimizedImageView
-                android:id="@+id/dream_overlay_wifi_status"
-                android:layout_width="@dimen/status_bar_wifi_signal_size"
-                android:layout_height="match_parent"
-                android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin"
-                android:visibility="gone"
-                app:layout_constraintEnd_toStartOf="@id/dream_overlay_battery" />
-
-            <com.android.systemui.battery.BatteryMeterView
-                android:id="@+id/dream_overlay_battery"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                app:layout_constraintEnd_toEndOf="parent" />
-
-        </androidx.constraintlayout.widget.ConstraintLayout>
-    </com.android.systemui.dreams.DreamOverlayStatusBarView>
+    <include layout="@layout/dream_overlay_status_bar_view" />
 </com.android.systemui.dreams.DreamOverlayContainerView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
new file mode 100644
index 0000000..813787e
--- /dev/null
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.android.systemui.dreams.DreamOverlayStatusBarView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/dream_overlay_status_bar"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/dream_overlay_status_bar_height"
+    android:paddingEnd="@dimen/dream_overlay_status_bar_margin"
+    android:paddingStart="@dimen/dream_overlay_status_bar_margin"
+    app:layout_constraintTop_toTopOf="parent">
+
+    <com.android.systemui.dreams.DreamOverlayDotImageView
+        android:id="@+id/dream_overlay_notification_indicator"
+        android:layout_width="@dimen/dream_overlay_notification_indicator_size"
+        android:layout_height="@dimen/dream_overlay_notification_indicator_size"
+        android:visibility="gone"
+        android:contentDescription="@string/dream_overlay_status_bar_notification_indicator"
+        app:dotColor="@android:color/white"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+    <LinearLayout
+        android:id="@+id/dream_overlay_system_status"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        app:layout_constraintEnd_toEndOf="parent">
+
+        <com.android.systemui.statusbar.AlphaOptimizedImageView
+            android:id="@+id/dream_overlay_assistant_guest_mode_enabled"
+            android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
+            android:layout_height="match_parent"
+            android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin"
+            android:src="@drawable/ic_account_circle"
+            android:tint="@android:color/white"
+            android:visibility="gone"
+            android:contentDescription=
+                "@string/dream_overlay_status_bar_assistant_guest_mode_enabled" />
+
+        <com.android.systemui.statusbar.AlphaOptimizedImageView
+            android:id="@+id/dream_overlay_alarm_set"
+            android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
+            android:layout_height="match_parent"
+            android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin"
+            android:src="@drawable/ic_alarm"
+            android:tint="@android:color/white"
+            android:visibility="gone"
+            android:contentDescription="@string/dream_overlay_status_bar_alarm_set" />
+
+        <com.android.systemui.statusbar.AlphaOptimizedImageView
+            android:id="@+id/dream_overlay_priority_mode"
+            android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
+            android:layout_height="match_parent"
+            android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin"
+            android:src="@drawable/ic_remove_circle"
+            android:tint="@android:color/white"
+            android:visibility="gone"
+            android:contentDescription="@string/dream_overlay_status_bar_priority_mode" />
+
+        <com.android.systemui.statusbar.AlphaOptimizedImageView
+            android:id="@+id/dream_overlay_wifi_status"
+            android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
+            android:layout_height="match_parent"
+            android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin"
+            android:src="@drawable/ic_signal_wifi_off"
+            android:visibility="gone"
+            android:contentDescription="@string/dream_overlay_status_bar_wifi_off" />
+
+        <com.android.systemui.dreams.DreamOverlayDotImageView
+            android:id="@+id/dream_overlay_camera_mic_off"
+            android:layout_width="@dimen/dream_overlay_camera_mic_off_indicator_size"
+            android:layout_height="@dimen/dream_overlay_camera_mic_off_indicator_size"
+            android:layout_gravity="center_vertical"
+            android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin"
+            android:visibility="gone"
+            android:contentDescription="@string/dream_overlay_status_bar_camera_mic_off"
+            app:dotColor="@color/dream_overlay_camera_mic_off_dot_color" />
+
+    </LinearLayout>
+</com.android.systemui.dreams.DreamOverlayStatusBarView>
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 2992859..c5e005c 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -199,5 +199,9 @@
     </declare-styleable>
 
     <attr name="overlayButtonTextColor" format="color" />
+
+    <declare-styleable name="DreamOverlayDotImageView">
+        <attr name="dotColor" format="color" />
+    </declare-styleable>
 </resources>
 
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index f4e7cf3..dc74700 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -225,4 +225,6 @@
     <color name="settingslib_track_off_color">@color/settingslib_track_off</color>
     <color name="connected_network_primary_color">#191C18</color>
     <color name="connected_network_secondary_color">#41493D</color>
+
+    <color name="dream_overlay_camera_mic_off_dot_color">#FCBE03</color>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 3704134..fcf60bf 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1332,12 +1332,16 @@
     <dimen name="fgs_manager_min_width_minor">100%</dimen>
 
     <!-- Dream overlay related dimensions -->
-    <dimen name="dream_overlay_status_bar_height">80dp</dimen>
+    <dimen name="dream_overlay_status_bar_height">60dp</dimen>
     <dimen name="dream_overlay_status_bar_margin">40dp</dimen>
     <dimen name="dream_overlay_status_icon_margin">8dp</dimen>
+    <dimen name="dream_overlay_status_bar_icon_size">
+        @*android:dimen/status_bar_system_icon_size</dimen>
     <!-- Height of the area at the top of the dream overlay to allow dragging down the notifications
          shade. -->
     <dimen name="dream_overlay_notifications_drag_area_height">100dp</dimen>
+    <dimen name="dream_overlay_camera_mic_off_indicator_size">8dp</dimen>
+    <dimen name="dream_overlay_notification_indicator_size">6dp</dimen>
 
     <!-- Dream overlay complications related dimensions -->
     <dimen name="dream_overlay_complication_clock_time_text_size">72sp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 3f80647..df16b0d 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2407,4 +2407,20 @@
     <string name="add_user_supervised" translatable="false">@*android:string/supervised_user_creation_label</string>
     <!-- Manage users - For system user management [CHAR LIMIT=40]  -->
     <string name="manage_users">Manage users</string>
+
+    <!-- Toast shown when a notification does not support dragging to split [CHAR LIMIT=NONE] -->
+    <string name="drag_split_not_supported">This notification does not support dragging to Splitscreen.</string>
+
+    <!-- Content description for the Wi-Fi off icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
+    <string name="dream_overlay_status_bar_wifi_off">Wi\u2011Fi unavailable</string>
+    <!-- Content description for the priority mode icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
+    <string name="dream_overlay_status_bar_priority_mode">Priority mode</string>
+    <!-- Content description for the alarm set icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
+    <string name="dream_overlay_status_bar_alarm_set">Alarm set</string>
+    <!-- Content description for the assistant guest mode enabled icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
+    <string name="dream_overlay_status_bar_assistant_guest_mode_enabled">Assistant guest mode enabled</string>
+    <!-- Content description for the camera and mic off icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
+    <string name="dream_overlay_status_bar_camera_mic_off">Camera and mic are off</string>
+    <!-- Content description for the camera and mic off icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
+    <string name="dream_overlay_status_bar_notification_indicator">There are notifications</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 9d65c38..f2eaa75 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -381,12 +381,19 @@
         <item name="android:buttonBarNeutralButtonStyle">@style/Widget.Dialog.Button.BorderButton</item>
         <item name="android:colorBackground">?androidprv:attr/colorSurface</item>
         <item name="android:alertDialogStyle">@style/AlertDialogStyle</item>
+        <item name="android:buttonBarStyle">@style/ButtonBarStyle</item>
+        <item name="android:buttonBarButtonStyle">@style/Widget.Dialog.Button.Large</item>
     </style>
 
     <style name="AlertDialogStyle" parent="@androidprv:style/AlertDialog.DeviceDefault">
         <item name="android:layout">@layout/alert_dialog_systemui</item>
     </style>
 
+    <style name="ButtonBarStyle" parent="@androidprv:style/DeviceDefault.ButtonBar.AlertDialog">
+        <item name="android:paddingTop">@dimen/dialog_button_bar_top_padding</item>
+        <item name="android:paddingBottom">@dimen/dialog_bottom_padding</item>
+    </style>
+
     <style name="Theme.SystemUI.Dialog.Alert" parent="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
 
     <style name="Theme.SystemUI.Dialog.GlobalActions" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar.Fullscreen">
@@ -962,6 +969,11 @@
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
+    <style name="Widget.Dialog.Button.Large">
+        <item name="android:background">@drawable/qs_dialog_btn_filled_large</item>
+        <item name="android:minHeight">56dp</item>
+    </style>
+
     <style name="MainSwitch.Settingslib" parent="@android:style/Theme.DeviceDefault">
         <item name="android:switchMinWidth">@dimen/settingslib_min_switch_width</item>
     </style>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 08b4d3f..2b1c47f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -103,8 +103,8 @@
     // enabled (since it's used to navigate back within the bubbled app, or to collapse the bubble
     // stack.
     public static final int SYSUI_STATE_BUBBLES_EXPANDED = 1 << 14;
-    // The global actions dialog is showing
-    public static final int SYSUI_STATE_GLOBAL_ACTIONS_SHOWING = 1 << 15;
+    // A SysUI dialog is showing.
+    public static final int SYSUI_STATE_DIALOG_SHOWING = 1 << 15;
     // The one-handed mode is active
     public static final int SYSUI_STATE_ONE_HANDED_ACTIVE = 1 << 16;
     // Allow system gesture no matter the system bar(s) is visible or not
@@ -140,7 +140,7 @@
             SYSUI_STATE_TRACING_ENABLED,
             SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED,
             SYSUI_STATE_BUBBLES_EXPANDED,
-            SYSUI_STATE_GLOBAL_ACTIONS_SHOWING,
+            SYSUI_STATE_DIALOG_SHOWING,
             SYSUI_STATE_ONE_HANDED_ACTIVE,
             SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
             SYSUI_STATE_IME_SHOWING,
@@ -166,7 +166,7 @@
         str.add((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0
                 ? "keygrd_occluded" : "");
         str.add((flags & SYSUI_STATE_BOUNCER_SHOWING) != 0 ? "bouncer_visible" : "");
-        str.add((flags & SYSUI_STATE_GLOBAL_ACTIONS_SHOWING) != 0 ? "global_actions" : "");
+        str.add((flags & SYSUI_STATE_DIALOG_SHOWING) != 0 ? "dialog_showing" : "");
         str.add((flags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0 ? "a11y_click" : "");
         str.add((flags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0 ? "a11y_long_click" : "");
         str.add((flags & SYSUI_STATE_TRACING_ENABLED) != 0 ? "tracing" : "");
@@ -256,7 +256,7 @@
     public static boolean isBackGestureDisabled(int sysuiStateFlags) {
         // Always allow when the bouncer/global actions is showing (even on top of the keyguard)
         if ((sysuiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0
-                || (sysuiStateFlags & SYSUI_STATE_GLOBAL_ACTIONS_SHOWING) != 0) {
+                || (sysuiStateFlags & SYSUI_STATE_DIALOG_SHOWING) != 0) {
             return false;
         }
         if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index b32c2b6..84b6ace 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -38,6 +38,7 @@
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
 import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.appops.AppOpsController;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -107,6 +108,7 @@
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.policy.AccessibilityController;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -372,6 +374,8 @@
     @Inject Lazy<AmbientState> mAmbientStateLazy;
     @Inject Lazy<GroupMembershipManager> mGroupMembershipManagerLazy;
     @Inject Lazy<GroupExpansionManager> mGroupExpansionManagerLazy;
+    @Inject Lazy<SystemUIDialogManager> mSystemUIDialogManagerLazy;
+    @Inject Lazy<DialogLaunchAnimator> mDialogLaunchAnimatorLazy;
 
     @Inject
     public Dependency() {
@@ -592,6 +596,8 @@
         mProviders.put(AmbientState.class, mAmbientStateLazy::get);
         mProviders.put(GroupMembershipManager.class, mGroupMembershipManagerLazy::get);
         mProviders.put(GroupExpansionManager.class, mGroupExpansionManagerLazy::get);
+        mProviders.put(SystemUIDialogManager.class, mSystemUIDialogManagerLazy::get);
+        mProviders.put(DialogLaunchAnimator.class, mDialogLaunchAnimatorLazy::get);
 
         Dependency.setInstance(this);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 0d20403..de03993 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -43,6 +43,7 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.Range;
+import android.util.Size;
 import android.view.Choreographer;
 import android.view.Display;
 import android.view.Gravity;
@@ -62,6 +63,8 @@
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
 
+import androidx.core.math.MathUtils;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.R;
@@ -166,6 +169,7 @@
     private final Rect mMagnificationFrameBoundary = new Rect();
     // The top Y of the system gesture rect at the bottom. Set to -1 if it is invalid.
     private int mSystemGestureTop = -1;
+    private int mMinWindowSize;
 
     private final WindowMagnificationAnimationController mAnimationController;
     private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
@@ -208,8 +212,10 @@
         mBounceEffectDuration = mResources.getInteger(
                 com.android.internal.R.integer.config_shortAnimTime);
         updateDimensions();
-        setMagnificationFrameWith(mWindowBounds, mWindowBounds.width() / 2,
-                mWindowBounds.height() / 2);
+
+        final Size windowSize = getDefaultWindowSizeWithWindowBounds(mWindowBounds);
+        setMagnificationFrame(windowSize.getWidth(), windowSize.getHeight(),
+                mWindowBounds.width() / 2, mWindowBounds.height() / 2);
         computeBounceAnimationScale();
 
         mMirrorWindowControl = mirrorWindowControl;
@@ -281,6 +287,8 @@
                 R.dimen.magnification_drag_view_size);
         mOuterBorderSize = mResources.getDimensionPixelSize(
                 R.dimen.magnification_outer_border_margin);
+        mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
     }
 
     private void computeBounceAnimationScale() {
@@ -414,9 +422,12 @@
             return false;
         }
         mWindowBounds.set(currentWindowBounds);
+        final Size windowSize = getDefaultWindowSizeWithWindowBounds(mWindowBounds);
         final float newCenterX = (getCenterX()) * mWindowBounds.width() / oldWindowBounds.width();
         final float newCenterY = (getCenterY()) * mWindowBounds.height() / oldWindowBounds.height();
-        setMagnificationFrameWith(mWindowBounds, (int) newCenterX, (int) newCenterY);
+
+        setMagnificationFrame(windowSize.getWidth(), windowSize.getHeight(), (int) newCenterX,
+                (int) newCenterY);
         calculateMagnificationFrameBoundary();
         return true;
     }
@@ -454,11 +465,6 @@
 
         mWindowBounds.set(currentWindowBounds);
 
-        calculateMagnificationFrameBoundary();
-
-        if (!isWindowVisible()) {
-            return;
-        }
         // Keep MirrorWindow position on the screen unchanged when device rotates 90°
         // clockwise or anti-clockwise.
 
@@ -469,14 +475,13 @@
         } else if (rotationDegree == 270) {
             matrix.postTranslate(0, mWindowBounds.height());
         }
-        // The rect of MirrorView is going to be transformed.
-        LayoutParams params =
-                (LayoutParams) mMirrorView.getLayoutParams();
-        mTmpRect.set(params.x, params.y, params.x + params.width, params.y + params.height);
-        final RectF transformedRect = new RectF(mTmpRect);
+
+        final RectF transformedRect = new RectF(mMagnificationFrame);
+        // The window frame is going to be transformed by the rotation matrix.
+        transformedRect.inset(-mMirrorSurfaceMargin, -mMirrorSurfaceMargin);
         matrix.mapRect(transformedRect);
-        moveWindowMagnifier(transformedRect.left - mTmpRect.left,
-                transformedRect.top - mTmpRect.top);
+        setWindowSizeAndCenter((int) transformedRect.width(), (int) transformedRect.height(),
+                (int) transformedRect.centerX(), (int) transformedRect.centerY());
     }
 
     /** Returns the rotation degree change of two {@link Surface.Rotation} */
@@ -573,16 +578,52 @@
         }
     }
 
-    private void setMagnificationFrameWith(Rect windowBounds, int centerX, int centerY) {
+    /**
+     * Sets the window size with given width and height in pixels without changing the
+     * window center. The width or the height will be clamped in the range
+     * [{@link #mMinWindowSize}, screen width or height].
+     *
+     * @param width the window width in pixels
+     * @param height the window height in pixels.
+     */
+    public void setWindowSize(int width, int height) {
+        setWindowSizeAndCenter(width, height, Float.NaN, Float.NaN);
+    }
+
+    void setWindowSizeAndCenter(int width, int height, float centerX, float centerY) {
+        width = MathUtils.clamp(width, mMinWindowSize, mWindowBounds.width());
+        height = MathUtils.clamp(height, mMinWindowSize, mWindowBounds.height());
+
+        if (Float.isNaN(centerX)) {
+            centerX = mMagnificationFrame.centerX();
+        }
+        if (Float.isNaN(centerX)) {
+            centerY = mMagnificationFrame.centerY();
+        }
+
+        final int frameWidth = width - 2 * mMirrorSurfaceMargin;
+        final int frameHeight = height - 2 * mMirrorSurfaceMargin;
+        setMagnificationFrame(frameWidth, frameHeight, (int) centerX, (int) centerY);
+        calculateMagnificationFrameBoundary();
+        // Correct the frame position to ensure it is inside the boundary.
+        updateMagnificationFramePosition(0, 0);
+        modifyWindowMagnification(true);
+    }
+
+    private void setMagnificationFrame(int width, int height, int centerX, int centerY) {
         // Sets the initial frame area for the mirror and place it to the given center on the
         // display.
+        final int initX = centerX - width / 2;
+        final int initY = centerY - height / 2;
+        mMagnificationFrame.set(initX, initY, initX + width, initY + height);
+    }
+
+    private Size getDefaultWindowSizeWithWindowBounds(Rect windowBounds) {
         int initSize = Math.min(windowBounds.width(), windowBounds.height()) / 2;
         initSize = Math.min(mResources.getDimensionPixelSize(R.dimen.magnification_max_frame_size),
                 initSize);
         initSize += 2 * mMirrorSurfaceMargin;
-        final int initX = centerX - initSize / 2;
-        final int initY = centerY - initSize / 2;
-        mMagnificationFrame.set(initX, initY, initX + initSize, initY + initSize);
+        return new Size(initSize, initSize);
     }
 
     /**
@@ -596,8 +637,7 @@
         }
         mTransaction.show(mMirrorSurface)
                 .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl());
-
-        modifyWindowMagnification(mTransaction);
+        modifyWindowMagnification(false);
     }
 
     private void addDragTouchListeners() {
@@ -615,18 +655,25 @@
     }
 
     /**
-     * Modifies the placement of the mirrored content when the position of mMirrorView is updated.
+     * Modifies the placement of the mirrored content when the position or size of mMirrorView is
+     * updated.
+     *
+     * @param computeWindowSize set to {@code true} to compute window size with
+     * {@link #mMagnificationFrame}.
      */
-    private void modifyWindowMagnification(SurfaceControl.Transaction t) {
+    private void modifyWindowMagnification(boolean computeWindowSize) {
         mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
-        updateMirrorViewLayout();
+        updateMirrorViewLayout(computeWindowSize);
     }
 
     /**
-     * Updates the layout params of MirrorView and translates MirrorView position when the view is
-     * moved close to the screen edges.
+     * Updates the layout params of MirrorView based on the size of {@link #mMagnificationFrame}
+     * and translates MirrorView position when the view is moved close to the screen edges;
+     *
+     * @param computeWindowSize set to {@code true} to compute window size with
+     * {@link #mMagnificationFrame}.
      */
-    private void updateMirrorViewLayout() {
+    private void updateMirrorViewLayout(boolean computeWindowSize) {
         if (!isWindowVisible()) {
             return;
         }
@@ -637,6 +684,10 @@
                 (LayoutParams) mMirrorView.getLayoutParams();
         params.x = mMagnificationFrame.left - mMirrorSurfaceMargin;
         params.y = mMagnificationFrame.top - mMirrorSurfaceMargin;
+        if (computeWindowSize) {
+            params.width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
+            params.height = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
+        }
 
         // Translates MirrorView position to make MirrorSurfaceView that is inside MirrorView
         // able to move close to the screen edges.
@@ -899,7 +950,7 @@
             createMirrorWindow();
             showControls();
         } else {
-            modifyWindowMagnification(mTransaction);
+            modifyWindowMagnification(false);
         }
     }
 
@@ -930,7 +981,7 @@
             return;
         }
         if (updateMagnificationFramePosition((int) offsetX, (int) offsetY)) {
-            modifyWindowMagnification(mTransaction);
+            modifyWindowMagnification(false);
         }
     }
 
@@ -1014,9 +1065,15 @@
         pw.println("      mOverlapWithGestureInsets:" + mOverlapWithGestureInsets);
         pw.println("      mScale:" + mScale);
         pw.println("      mMirrorViewBounds:" + (isWindowVisible() ? mMirrorViewBounds : "empty"));
+        pw.println("      mMagnificationFrameBoundary:"
+                + (isWindowVisible() ? mMagnificationFrameBoundary : "empty"));
+        pw.println("      mMagnificationFrame:"
+                + (isWindowVisible() ? mMagnificationFrame : "empty"));
         pw.println("      mSourceBounds:"
                  + (isWindowVisible() ? mSourceBounds : "empty"));
         pw.println("      mSystemGestureTop:" + mSystemGestureTop);
+        pw.println("      mMagnificationFrameOffsetX:" + mMagnificationFrameOffsetX);
+        pw.println("      mMagnificationFrameOffsetY:" + mMagnificationFrameOffsetY);
     }
 
     private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayDotImageView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayDotImageView.java
new file mode 100644
index 0000000..02a8b39a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayDotImageView.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.AlphaOptimizedImageView;
+
+/**
+ * An {@link AlphaOptimizedImageView} that is responsible for rendering a dot. Used by
+ * {@link DreamOverlayStatusBarView}.
+ */
+public class DreamOverlayDotImageView extends AlphaOptimizedImageView {
+    private final @ColorInt int mDotColor;
+
+    public DreamOverlayDotImageView(Context context) {
+        this(context, null);
+    }
+
+    public DreamOverlayDotImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DreamOverlayDotImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public DreamOverlayDotImageView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
+                R.styleable.DreamOverlayDotImageView, 0, 0);
+
+        try {
+            mDotColor = a.getColor(R.styleable.DreamOverlayDotImageView_dotColor, Color.WHITE);
+        } finally {
+            a.recycle();
+        }
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        setImageDrawable(new DotDrawable(mDotColor));
+    }
+
+    private static class DotDrawable extends Drawable {
+        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        private Bitmap mDotBitmap;
+        private final Rect mBounds = new Rect();
+        private final @ColorInt int mDotColor;
+
+        DotDrawable(@ColorInt int color) {
+            mDotColor = color;
+        }
+
+        @Override
+        public void draw(@NonNull Canvas canvas) {
+            if (mBounds.isEmpty()) {
+                return;
+            }
+
+            if (mDotBitmap == null) {
+                mDotBitmap = createBitmap(mBounds.width(), mBounds.height());
+            }
+
+            canvas.drawBitmap(mDotBitmap, null, mBounds, mPaint);
+        }
+
+        @Override
+        protected void onBoundsChange(Rect bounds) {
+            super.onBoundsChange(bounds);
+            mBounds.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
+            // Make sure to regenerate the dot bitmap when the bounds change.
+            mDotBitmap = null;
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+        }
+
+        @Override
+        public void setColorFilter(@Nullable ColorFilter colorFilter) {
+        }
+
+        @Override
+        public int getOpacity() {
+            return 0;
+        }
+
+        private Bitmap createBitmap(int width, int height) {
+            Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+            Canvas canvas = new Canvas(bitmap);
+            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+            paint.setColor(mDotColor);
+            canvas.drawCircle(width / 2.f, height / 2.f, Math.min(width, height) / 2.f, paint);
+            return bitmap;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
index 9847ef6..2d96920 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
@@ -25,17 +25,13 @@
 
 import com.android.internal.util.Preconditions;
 import com.android.systemui.R;
-import com.android.systemui.battery.BatteryMeterView;
-import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
 /**
  * {@link DreamOverlayStatusBarView} is the view responsible for displaying the status bar in a
- * dream. The status bar includes status icons such as battery and wifi.
+ * dream. The status bar displays conditional status icons such as "priority mode" and "no wifi".
  */
-public class DreamOverlayStatusBarView extends ConstraintLayout implements
-        BatteryStateChangeCallback {
+public class DreamOverlayStatusBarView extends ConstraintLayout {
 
-    private BatteryMeterView mBatteryView;
     private ImageView mWifiStatusView;
 
     public DreamOverlayStatusBarView(Context context) {
@@ -59,20 +55,8 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        mBatteryView = Preconditions.checkNotNull(findViewById(R.id.dream_overlay_battery),
-                "R.id.dream_overlay_battery must not be null");
         mWifiStatusView = Preconditions.checkNotNull(findViewById(R.id.dream_overlay_wifi_status),
                 "R.id.dream_overlay_wifi_status must not be null");
-
-        mWifiStatusView.setImageDrawable(getContext().getDrawable(R.drawable.ic_signal_wifi_off));
-    }
-
-    /**
-     * Whether to show the battery percent text next to the battery status icons.
-     * @param show True if the battery percent text should be shown.
-     */
-    void showBatteryPercentText(boolean show) {
-        mBatteryView.setForceShowPercent(show);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index 5674b9f..ed82ab0 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -17,24 +17,19 @@
 package com.android.systemui.dreams;
 
 import android.annotation.IntDef;
-import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 
-import com.android.systemui.battery.BatteryMeterViewController;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
-import com.android.systemui.dreams.dagger.DreamOverlayModule;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.ViewController;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 import javax.inject.Inject;
-import javax.inject.Named;
 
 /**
  * View controller for {@link DreamOverlayStatusBarView}.
@@ -52,21 +47,7 @@
     private static final int WIFI_STATUS_UNAVAILABLE = 1;
     private static final int WIFI_STATUS_AVAILABLE = 2;
 
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = { "BATTERY_STATUS_" }, value = {
-            BATTERY_STATUS_UNKNOWN,
-            BATTERY_STATUS_NOT_CHARGING,
-            BATTERY_STATUS_CHARGING
-    })
-    private @interface BatteryStatus {}
-    private static final int BATTERY_STATUS_UNKNOWN = 0;
-    private static final int BATTERY_STATUS_NOT_CHARGING = 1;
-    private static final int BATTERY_STATUS_CHARGING = 2;
-
-    private final BatteryController mBatteryController;
-    private final BatteryMeterViewController mBatteryMeterViewController;
     private final ConnectivityManager mConnectivityManager;
-    private final boolean mShowPercentAvailable;
 
     private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder()
             .clearCapabilities()
@@ -91,43 +72,18 @@
         }
     };
 
-    private final BatteryController.BatteryStateChangeCallback mBatteryStateChangeCallback =
-            new BatteryController.BatteryStateChangeCallback() {
-                @Override
-                public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
-                    DreamOverlayStatusBarViewController.this.onBatteryLevelChanged(charging);
-                }
-            };
-
     private @WifiStatus int mWifiStatus = WIFI_STATUS_UNKNOWN;
-    private @BatteryStatus int mBatteryStatus = BATTERY_STATUS_UNKNOWN;
 
     @Inject
     public DreamOverlayStatusBarViewController(
-            Context context,
             DreamOverlayStatusBarView view,
-            BatteryController batteryController,
-            @Named(DreamOverlayModule.DREAM_OVERLAY_BATTERY_CONTROLLER)
-                    BatteryMeterViewController batteryMeterViewController,
             ConnectivityManager connectivityManager) {
         super(view);
-        mBatteryController = batteryController;
-        mBatteryMeterViewController = batteryMeterViewController;
         mConnectivityManager = connectivityManager;
-
-        mShowPercentAvailable = context.getResources().getBoolean(
-                com.android.internal.R.bool.config_battery_percentage_setting_available);
-    }
-
-    @Override
-    protected void onInit() {
-        super.onInit();
-        mBatteryMeterViewController.init();
     }
 
     @Override
     protected void onViewAttached() {
-        mBatteryController.addCallback(mBatteryStateChangeCallback);
         mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback);
 
         NetworkCapabilities capabilities =
@@ -140,7 +96,6 @@
 
     @Override
     protected void onViewDetached() {
-        mBatteryController.removeCallback(mBatteryStateChangeCallback);
         mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
     }
 
@@ -155,18 +110,4 @@
             mView.showWifiStatus(mWifiStatus == WIFI_STATUS_UNAVAILABLE);
         }
     }
-
-    /**
-     * The battery level has changed. Update the battery status icon as appropriate.
-     * @param charging Whether the battery is currently charging.
-     */
-    private void onBatteryLevelChanged(boolean charging) {
-        final int newBatteryStatus =
-                charging ? BATTERY_STATUS_CHARGING : BATTERY_STATUS_NOT_CHARGING;
-        if (mBatteryStatus != newBatteryStatus) {
-            mBatteryStatus = newBatteryStatus;
-            mView.showBatteryPercentText(
-                    mBatteryStatus == BATTERY_STATUS_CHARGING && mShowPercentAvailable);
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index 4eb5cb9..839a05e 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -16,9 +16,7 @@
 
 package com.android.systemui.dreams.dagger;
 
-import android.content.ContentResolver;
 import android.content.res.Resources;
-import android.os.Handler;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
@@ -28,15 +26,9 @@
 
 import com.android.internal.util.Preconditions;
 import com.android.systemui.R;
-import com.android.systemui.battery.BatteryMeterView;
-import com.android.systemui.battery.BatteryMeterViewController;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayContainerView;
 import com.android.systemui.dreams.DreamOverlayStatusBarView;
-import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.tuner.TunerService;
 
 import javax.inject.Named;
 
@@ -47,9 +39,6 @@
 /** Dagger module for {@link DreamOverlayComponent}. */
 @Module
 public abstract class DreamOverlayModule {
-    private static final String DREAM_OVERLAY_BATTERY_VIEW = "dream_overlay_battery_view";
-    public static final String DREAM_OVERLAY_BATTERY_CONTROLLER =
-            "dream_overlay_battery_controller";
     public static final String DREAM_OVERLAY_CONTENT_VIEW = "dream_overlay_content_view";
     public static final String MAX_BURN_IN_OFFSET = "max_burn_in_offset";
     public static final String BURN_IN_PROTECTION_UPDATE_INTERVAL =
@@ -86,37 +75,6 @@
     /** */
     @Provides
     @DreamOverlayComponent.DreamOverlayScope
-    @Named(DREAM_OVERLAY_BATTERY_VIEW)
-    static BatteryMeterView providesBatteryMeterView(DreamOverlayContainerView view) {
-        return Preconditions.checkNotNull(view.findViewById(R.id.dream_overlay_battery),
-                "R.id.battery must not be null");
-    }
-
-    /** */
-    @Provides
-    @DreamOverlayComponent.DreamOverlayScope
-    @Named(DREAM_OVERLAY_BATTERY_CONTROLLER)
-    static BatteryMeterViewController providesBatteryMeterViewController(
-            @Named(DREAM_OVERLAY_BATTERY_VIEW) BatteryMeterView batteryMeterView,
-            ConfigurationController configurationController,
-            TunerService tunerService,
-            BroadcastDispatcher broadcastDispatcher,
-            @Main Handler mainHandler,
-            ContentResolver contentResolver,
-            BatteryController batteryController) {
-        return new BatteryMeterViewController(
-                batteryMeterView,
-                configurationController,
-                tunerService,
-                broadcastDispatcher,
-                mainHandler,
-                contentResolver,
-                batteryController);
-    }
-
-    /** */
-    @Provides
-    @DreamOverlayComponent.DreamOverlayScope
     @Named(MAX_BURN_IN_OFFSET)
     static int providesMaxBurnInOffset(@Main Resources resources) {
         return resources.getDimensionPixelSize(R.dimen.default_burn_in_prevention_offset);
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 84fa6a6..e3886cd 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -27,7 +27,6 @@
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -117,7 +116,6 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.GlobalActions.GlobalActionsManager;
 import com.android.systemui.plugins.GlobalActionsPanelPlugin;
 import com.android.systemui.scrim.ScrimDrawable;
@@ -125,7 +123,6 @@
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.telephony.TelephonyListenerManager;
@@ -200,7 +197,6 @@
     private final TelecomManager mTelecomManager;
     private final MetricsLogger mMetricsLogger;
     private final UiEventLogger mUiEventLogger;
-    private final SysUiState mSysUiState;
 
     // Used for RingerModeTracker
     private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
@@ -241,7 +237,6 @@
     protected Handler mMainHandler;
     private int mSmallestScreenWidthDp;
     private final Optional<StatusBar> mStatusBarOptional;
-    private final SystemUIDialogManager mDialogManager;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final DialogLaunchAnimator mDialogLaunchAnimator;
 
@@ -347,13 +342,11 @@
             @Background Executor backgroundExecutor,
             UiEventLogger uiEventLogger,
             RingerModeTracker ringerModeTracker,
-            SysUiState sysUiState,
             @Main Handler handler,
             PackageManager packageManager,
             Optional<StatusBar> statusBarOptional,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            DialogLaunchAnimator dialogLaunchAnimator,
-            SystemUIDialogManager dialogManager) {
+            DialogLaunchAnimator dialogLaunchAnimator) {
         mContext = context;
         mWindowManagerFuncs = windowManagerFuncs;
         mAudioManager = audioManager;
@@ -379,13 +372,11 @@
         mIWindowManager = iWindowManager;
         mBackgroundExecutor = backgroundExecutor;
         mRingerModeTracker = ringerModeTracker;
-        mSysUiState = sysUiState;
         mMainHandler = handler;
         mSmallestScreenWidthDp = resources.getConfiguration().smallestScreenWidthDp;
         mStatusBarOptional = statusBarOptional;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mDialogLaunchAnimator = dialogLaunchAnimator;
-        mDialogManager = dialogManager;
 
         // receive broadcasts
         IntentFilter filter = new IntentFilter();
@@ -682,11 +673,10 @@
 
         ActionsDialogLite dialog = new ActionsDialogLite(mContext,
                 com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActionsLite,
-                mAdapter, mOverflowAdapter, mSysuiColorExtractor,
-                mStatusBarService, mNotificationShadeWindowController,
-                mSysUiState, this::onRefresh, mKeyguardShowing, mPowerAdapter, mUiEventLogger,
-                mStatusBarOptional, mKeyguardUpdateMonitor, mLockPatternUtils,
-                mDialogManager);
+                mAdapter, mOverflowAdapter, mSysuiColorExtractor, mStatusBarService,
+                mNotificationShadeWindowController, this::onRefresh, mKeyguardShowing,
+                mPowerAdapter, mUiEventLogger, mStatusBarOptional, mKeyguardUpdateMonitor,
+                mLockPatternUtils);
 
         dialog.setOnDismissListener(this);
         dialog.setOnShowListener(this);
@@ -2165,7 +2155,6 @@
         private boolean mKeyguardShowing;
         protected float mScrimAlpha;
         protected final NotificationShadeWindowController mNotificationShadeWindowController;
-        protected final SysUiState mSysUiState;
         private ListPopupWindow mOverflowPopup;
         private Dialog mPowerOptionsDialog;
         protected final Runnable mOnRefreshCallback;
@@ -2226,15 +2215,13 @@
                 MyOverflowAdapter overflowAdapter,
                 SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService,
                 NotificationShadeWindowController notificationShadeWindowController,
-                SysUiState sysuiState, Runnable onRefreshCallback, boolean keyguardShowing,
+                Runnable onRefreshCallback, boolean keyguardShowing,
                 MyPowerOptionsAdapter powerAdapter, UiEventLogger uiEventLogger,
-                Optional<StatusBar> statusBarOptional,
-                KeyguardUpdateMonitor keyguardUpdateMonitor, LockPatternUtils lockPatternUtils,
-                SystemUIDialogManager systemUiDialogManager) {
+                Optional<StatusBar> statusBarOptional, KeyguardUpdateMonitor keyguardUpdateMonitor,
+                LockPatternUtils lockPatternUtils) {
             // We set dismissOnDeviceLock to false because we have a custom broadcast receiver to
             // dismiss this dialog when the device is locked.
-            super(context, themeRes, false /* dismissOnDeviceLock */,
-                    systemUiDialogManager);
+            super(context, themeRes, false /* dismissOnDeviceLock */);
             mContext = context;
             mAdapter = adapter;
             mOverflowAdapter = overflowAdapter;
@@ -2242,7 +2229,6 @@
             mColorExtractor = sysuiColorExtractor;
             mStatusBarService = statusBarService;
             mNotificationShadeWindowController = notificationShadeWindowController;
-            mSysUiState = sysuiState;
             mOnRefreshCallback = onRefreshCallback;
             mKeyguardShowing = keyguardShowing;
             mUiEventLogger = uiEventLogger;
@@ -2463,8 +2449,6 @@
         public void show() {
             super.show();
             mNotificationShadeWindowController.setRequestTopUi(true, TAG);
-            mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, true)
-                    .commitUpdate(mContext.getDisplayId());
 
             // By default this dialog windowAnimationStyle is null, and therefore windowAnimations
             // should be equal to 0 which means we need to animate the dialog in-window. If it's not
@@ -2563,9 +2547,6 @@
             dismissPowerOptions();
 
             mNotificationShadeWindowController.setRequestTopUi(false, TAG);
-            mSysUiState.setFlag(SYSUI_STATE_GLOBAL_ACTIONS_SHOWING, false)
-                    .commitUpdate(mContext.getDisplayId());
-
             super.dismiss();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 7bb5454..04a324b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -56,7 +56,6 @@
 
 import com.android.systemui.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 /**
  * Base dialog for media output UI
@@ -99,9 +98,8 @@
         }
     };
 
-    public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController,
-            SystemUIDialogManager dialogManager) {
-        super(context, R.style.Theme_SystemUI_Dialog_Media, dialogManager);
+    public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
+        super(context, R.style.Theme_SystemUI_Dialog_Media);
 
         // Save the context that is wrapped with our theme.
         mContext = getContext();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 7bc0f52..e929b5e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -66,7 +66,6 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -91,7 +90,6 @@
     private final ShadeController mShadeController;
     private final ActivityStarter mActivityStarter;
     private final DialogLaunchAnimator mDialogLaunchAnimator;
-    private final SystemUIDialogManager mDialogManager;
     private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
     private final boolean mAboveStatusbar;
     private final boolean mVolumeAdjustmentForRemoteGroupSessions;
@@ -119,7 +117,7 @@
             boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager
             lbm, ShadeController shadeController, ActivityStarter starter,
             CommonNotifCollection notifCollection, UiEventLogger uiEventLogger,
-            DialogLaunchAnimator dialogLaunchAnimator, SystemUIDialogManager dialogManager) {
+            DialogLaunchAnimator dialogLaunchAnimator) {
         mContext = context;
         mPackageName = packageName;
         mMediaSessionManager = mediaSessionManager;
@@ -135,7 +133,6 @@
         mDialogLaunchAnimator = dialogLaunchAnimator;
         mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
-        mDialogManager = dialogManager;
         mColorActiveItem = Utils.getColorStateListDefaultColor(mContext,
                 R.color.media_dialog_active_item_main_content);
         mColorInactiveItem = Utils.getColorStateListDefaultColor(mContext,
@@ -610,10 +607,9 @@
         // We show the output group dialog from the output dialog.
         MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
                 mAboveStatusbar, mMediaSessionManager, mLocalBluetoothManager, mShadeController,
-                mActivityStarter, mNotifCollection, mUiEventLogger, mDialogLaunchAnimator,
-                mDialogManager);
+                mActivityStarter, mNotifCollection, mUiEventLogger, mDialogLaunchAnimator);
         MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar,
-                controller, mDialogManager);
+                controller);
         mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 4e9da55..7696a1f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -29,7 +29,6 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 /**
  * Dialog for media output transferring.
@@ -39,9 +38,8 @@
     final UiEventLogger mUiEventLogger;
 
     MediaOutputDialog(Context context, boolean aboveStatusbar, MediaOutputController
-            mediaOutputController, UiEventLogger uiEventLogger,
-            SystemUIDialogManager dialogManager) {
-        super(context, mediaOutputController, dialogManager);
+            mediaOutputController, UiEventLogger uiEventLogger) {
+        super(context, mediaOutputController);
         mUiEventLogger = uiEventLogger;
         mAdapter = new MediaOutputAdapter(mMediaOutputController, this);
         if (!aboveStatusbar) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index e1e7fa3..9e252ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -25,7 +25,6 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.phone.ShadeController
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import javax.inject.Inject
 
 /**
@@ -39,8 +38,7 @@
     private val starter: ActivityStarter,
     private val notifCollection: CommonNotifCollection,
     private val uiEventLogger: UiEventLogger,
-    private val dialogLaunchAnimator: DialogLaunchAnimator,
-    private val dialogManager: SystemUIDialogManager
+    private val dialogLaunchAnimator: DialogLaunchAnimator
 ) {
     companion object {
         var mediaOutputDialog: MediaOutputDialog? = null
@@ -53,9 +51,8 @@
 
         val controller = MediaOutputController(context, packageName, aboveStatusBar,
             mediaSessionManager, lbm, shadeController, starter, notifCollection,
-            uiEventLogger, dialogLaunchAnimator, dialogManager)
-        val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger,
-                dialogManager)
+            uiEventLogger, dialogLaunchAnimator)
+        val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger)
         mediaOutputDialog = dialog
 
         // Show the dialog.
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java
index 9f752b9..f1c6601 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java
@@ -25,7 +25,6 @@
 import androidx.core.graphics.drawable.IconCompat;
 
 import com.android.systemui.R;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 /**
  * Dialog for media output group.
@@ -34,8 +33,8 @@
 public class MediaOutputGroupDialog extends MediaOutputBaseDialog {
 
     MediaOutputGroupDialog(Context context, boolean aboveStatusbar, MediaOutputController
-            mediaOutputController, SystemUIDialogManager dialogManager) {
-        super(context, mediaOutputController, dialogManager);
+            mediaOutputController) {
+        super(context, mediaOutputController);
         mMediaOutputController.resetGroupMediaDevices();
         mAdapter = new MediaOutputGroupAdapter(mMediaOutputController);
         if (!aboveStatusbar) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index e088f54..5d6bbae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -202,11 +202,12 @@
                         mActivityStarter
                                 .postStartActivityDismissingKeyguard(getLongClickIntent(), 0,
                                         controller);
-                    });
+                    }, R.style.Theme_SystemUI_Dialog, false /* showProgressBarWhenEmpty */);
             holder.init(dialog);
             SystemUIDialog.setShowForAllUsers(dialog, true);
             SystemUIDialog.registerDismissListener(dialog);
             SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing());
+            SystemUIDialog.setDialogSize(dialog);
 
             mUiHandler.post(() -> {
                 if (view != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
index 06b739b..c2c40d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
@@ -17,6 +17,11 @@
 
 package com.android.systemui.statusbar.notification.row;
 
+import static android.widget.Toast.LENGTH_SHORT;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.app.Notification;
 import android.app.PendingIntent;
@@ -33,13 +38,15 @@
 import android.util.Log;
 import android.view.DragEvent;
 import android.view.HapticFeedbackConstants;
+import android.view.SurfaceControl;
 import android.view.View;
 import android.widget.ImageView;
+import android.widget.Toast;
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -55,12 +62,15 @@
 
     private final Context mContext;
     private final HeadsUpManager mHeadsUpManager;
+    private final ShadeController mShadeController;
 
     @Inject
     public ExpandableNotificationRowDragController(Context context,
-            HeadsUpManager headsUpManager) {
+            HeadsUpManager headsUpManager,
+            ShadeController shadeController) {
         mContext = context;
         mHeadsUpManager = headsUpManager;
+        mShadeController = shadeController;
 
         init();
     }
@@ -87,6 +97,16 @@
         final PendingIntent contentIntent = notification.contentIntent != null
                 ? notification.contentIntent
                 : notification.fullScreenIntent;
+        if (contentIntent == null) {
+            if (!enr.isPinned()) {
+                // We dismiss the shade for consistency, but also because toasts currently don't
+                // show above the shade
+                dismissShade();
+            }
+            Toast.makeText(mContext, R.string.drag_split_not_supported, LENGTH_SHORT)
+                 .show();
+            return;
+        }
         Bitmap iconBitmap = getBitmapFromDrawable(
                 getPkgIcon(enr.getEntry().getSbn().getPackageName()));
 
@@ -97,15 +117,30 @@
         ClipDescription clipDescription = new ClipDescription("Drag And Drop",
                 new String[]{ClipDescription.MIMETYPE_APPLICATION_ACTIVITY});
         Intent dragIntent = new Intent();
-        dragIntent.putExtra("android.intent.extra.PENDING_INTENT", contentIntent);
+        dragIntent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, contentIntent);
         dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
         ClipData.Item item = new ClipData.Item(dragIntent);
         ClipData dragData = new ClipData(clipDescription, item);
         View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot);
         view.setOnDragListener(getDraggedViewDragListener());
-        view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL);
+        boolean result = view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL
+                | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
+        if (result) {
+            view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+            if (enr.isPinned()) {
+                mHeadsUpManager.releaseAllImmediately();
+            } else {
+                dismissShade();
+            }
+        }
     }
 
+    private void dismissShade() {
+        // Speed up dismissing the shade since the drag needs to be handled by
+        // the shell layer underneath
+        mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */,
+                false /* delayed */, 1.1f /* speedUpFactor */);
+    }
 
     private Drawable getPkgIcon(String pkgName) {
         Drawable pkgicon = null;
@@ -145,16 +180,6 @@
         return (view, dragEvent) -> {
             switch (dragEvent.getAction()) {
                 case DragEvent.ACTION_DRAG_STARTED:
-                    view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
-                    if (view instanceof ExpandableNotificationRow) {
-                        ExpandableNotificationRow enr = (ExpandableNotificationRow) view;
-                        if (enr.isPinned()) {
-                            mHeadsUpManager.releaseAllImmediately();
-                        } else {
-                            Dependency.get(ShadeController.class).animateCollapsePanels(
-                                    CommandQueue.FLAG_EXCLUDE_NONE, true /* force */);
-                        }
-                    }
                     return true;
                 case DragEvent.ACTION_DRAG_ENDED:
                     if (dragEvent.getResult()) {
@@ -162,10 +187,55 @@
                             ExpandableNotificationRow enr = (ExpandableNotificationRow) view;
                             enr.dragAndDropSuccess();
                         }
+                    } else {
+                        // Fade out the drag surface in place instead of animating back to the
+                        // start position now that the shade is closed
+                        fadeOutAndRemoveDragSurface(dragEvent);
                     }
+                    // Clear the drag listener set above
+                    view.setOnDragListener(null);
                     return true;
             }
             return false;
         };
     }
+
+    private void fadeOutAndRemoveDragSurface(DragEvent dragEvent) {
+        SurfaceControl dragSurface = dragEvent.getDragSurface();
+        SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+        ValueAnimator returnAnimator = ValueAnimator.ofFloat(0f, 1f);
+        returnAnimator.setDuration(200);
+        returnAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        returnAnimator.addUpdateListener(animation -> {
+            float t = animation.getAnimatedFraction();
+            float alpha = 1f - t;
+            tx.setAlpha(dragSurface, alpha);
+            tx.apply();
+        });
+        returnAnimator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCanceled = false;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                cleanUpSurface();
+                mCanceled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCanceled) {
+                    // Already handled above
+                    return;
+                }
+                cleanUpSurface();
+            }
+
+            private void cleanUpSurface() {
+                tx.remove(dragSurface);
+                tx.apply();
+                tx.close();
+            }
+        });
+        returnAnimator.start();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 6d774838..05fba54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -28,6 +28,8 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.assist.AssistManager;
@@ -46,8 +48,11 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.unfold.FoldAodAnimationController;
+import com.android.systemui.unfold.SysUIUnfoldComponent;
 
 import java.util.ArrayList;
+import java.util.Optional;
 
 import javax.inject.Inject;
 
@@ -73,6 +78,8 @@
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final DeviceProvisionedController mDeviceProvisionedController;
+    @Nullable
+    private final FoldAodAnimationController mFoldAodAnimationController;
     private final HeadsUpManagerPhone mHeadsUpManagerPhone;
     private final BatteryController mBatteryController;
     private final ScrimController mScrimController;
@@ -105,6 +112,7 @@
             Lazy<AssistManager> assistManagerLazy,
             DozeScrimController dozeScrimController, KeyguardUpdateMonitor keyguardUpdateMonitor,
             PulseExpansionHandler pulseExpansionHandler,
+            Optional<SysUIUnfoldComponent> sysUIUnfoldComponent,
             NotificationShadeWindowController notificationShadeWindowController,
             NotificationWakeUpCoordinator notificationWakeUpCoordinator,
             AuthController authController,
@@ -128,6 +136,8 @@
         mNotificationWakeUpCoordinator = notificationWakeUpCoordinator;
         mAuthController = authController;
         mNotificationIconAreaController = notificationIconAreaController;
+        mFoldAodAnimationController = sysUIUnfoldComponent
+                .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
     }
 
     // TODO: we should try to not pass status bar in here if we can avoid it.
@@ -215,6 +225,9 @@
 
         mStatusBarStateController.setIsDozing(dozing);
         mNotificationShadeWindowViewController.setDozing(dozing);
+        if (mFoldAodAnimationController != null) {
+            mFoldAodAnimationController.setIsDozing(dozing);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 9722528..79d646c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -42,7 +42,10 @@
 
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.shared.system.QuickStepContract;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -64,7 +67,8 @@
     private final Context mContext;
     @Nullable private final DismissReceiver mDismissReceiver;
     private final Handler mHandler = new Handler();
-    @Nullable private final SystemUIDialogManager mDialogManager;
+    private final SystemUIDialogManager mDialogManager;
+    private final SysUiState mSysUiState;
 
     private int mLastWidth = Integer.MIN_VALUE;
     private int mLastHeight = Integer.MIN_VALUE;
@@ -77,24 +81,11 @@
         this(context, R.style.Theme_SystemUI_Dialog);
     }
 
-    public SystemUIDialog(Context context, SystemUIDialogManager dialogManager) {
-        this(context, R.style.Theme_SystemUI_Dialog, true, dialogManager);
-    }
-
     public SystemUIDialog(Context context, int theme) {
         this(context, theme, true /* dismissOnDeviceLock */);
     }
 
-    public SystemUIDialog(Context context, int theme, SystemUIDialogManager dialogManager) {
-        this(context, theme, true /* dismissOnDeviceLock */, dialogManager);
-    }
-
     public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) {
-        this(context, theme, dismissOnDeviceLock, null);
-    }
-
-    public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock,
-            @Nullable SystemUIDialogManager dialogManager) {
         super(context, theme);
         mContext = context;
 
@@ -104,7 +95,12 @@
         getWindow().setAttributes(attrs);
 
         mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null;
-        mDialogManager = dialogManager;
+
+        // TODO(b/219008720): Remove those calls to Dependency.get by introducing a
+        // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set
+        // the content and attach listeners.
+        mDialogManager = Dependency.get(SystemUIDialogManager.class);
+        mSysUiState = Dependency.get(SysUiState.class);
     }
 
     @Override
@@ -174,13 +170,11 @@
             mDismissReceiver.register();
         }
 
-        if (mDialogManager != null) {
-            mDialogManager.setShowing(this, true);
-        }
-
         // Listen for configuration changes to resize this dialog window. This is mostly necessary
         // for foldables that often go from large <=> small screen when folding/unfolding.
         ViewRootImpl.addConfigCallback(this);
+        mDialogManager.setShowing(this, true);
+        mSysUiState.setFlag(QuickStepContract.SYSUI_STATE_DIALOG_SHOWING, true);
     }
 
     @Override
@@ -191,11 +185,9 @@
             mDismissReceiver.unregister();
         }
 
-        if (mDialogManager != null) {
-            mDialogManager.setShowing(this, false);
-        }
-
         ViewRootImpl.removeConfigCallback(this);
+        mDialogManager.setShowing(this, false);
+        mSysUiState.setFlag(QuickStepContract.SYSUI_STATE_DIALOG_SHOWING, false);
     }
 
     public void setShowForAllUsers(boolean show) {
@@ -401,10 +393,13 @@
         private final Dialog mDialog;
         private boolean mRegistered;
         private final BroadcastDispatcher mBroadcastDispatcher;
+        private final DialogLaunchAnimator mDialogLaunchAnimator;
 
         DismissReceiver(Dialog dialog) {
             mDialog = dialog;
+            // TODO(b/219008720): Remove those calls to Dependency.get.
             mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
+            mDialogLaunchAnimator = Dependency.get(DialogLaunchAnimator.class);
         }
 
         void register() {
@@ -421,6 +416,10 @@
 
         @Override
         public void onReceive(Context context, Intent intent) {
+            // These broadcast are usually received when locking the device, swiping up to home
+            // (which collapses the shade), etc. In those cases, we usually don't want to animate
+            // back into the view.
+            mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
             mDialog.dismiss();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
index 2a9076e..e2374ad 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
@@ -16,9 +16,12 @@
 
 package com.android.systemui.unfold
 
+import android.content.Context
+import android.hardware.devicestate.DeviceStateManager
 import android.os.Handler
 import android.os.PowerManager
 import android.provider.Settings
+import androidx.core.view.OneShotPreDrawListener
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.statusbar.LightRevealScrim
@@ -27,6 +30,8 @@
 import com.android.systemui.statusbar.policy.CallbackController
 import com.android.systemui.unfold.FoldAodAnimationController.FoldAodAnimationStatus
 import com.android.systemui.util.settings.GlobalSettings
+import java.util.concurrent.Executor
+import java.util.function.Consumer
 import javax.inject.Inject
 
 /**
@@ -38,13 +43,21 @@
 @Inject
 constructor(
     @Main private val handler: Handler,
+    @Main private val executor: Executor,
+    private val context: Context,
+    private val deviceStateManager: DeviceStateManager,
     private val wakefulnessLifecycle: WakefulnessLifecycle,
     private val globalSettings: GlobalSettings
 ) : CallbackController<FoldAodAnimationStatus>, ScreenOffAnimation, WakefulnessLifecycle.Observer {
 
-    private var alwaysOnEnabled: Boolean = false
-    private var isScrimOpaque: Boolean = false
     private lateinit var statusBar: StatusBar
+
+    private var isFolded = false
+    private var isFoldHandled = true
+
+    private var alwaysOnEnabled: Boolean = false
+    private var isDozing: Boolean = false
+    private var isScrimOpaque: Boolean = false
     private var pendingScrimReadyCallback: Runnable? = null
 
     private var shouldPlayAnimation = false
@@ -62,6 +75,7 @@
     override fun initialize(statusBar: StatusBar, lightRevealScrim: LightRevealScrim) {
         this.statusBar = statusBar
 
+        deviceStateManager.registerCallback(executor, FoldListener())
         wakefulnessLifecycle.addObserver(this)
     }
 
@@ -84,7 +98,7 @@
     override fun onStartedWakingUp() {
         if (isAnimationPlaying) {
             handler.removeCallbacks(startAnimationRunnable)
-            statusBar.notificationPanelViewController.cancelFoldToAodAnimation();
+            statusBar.notificationPanelViewController.cancelFoldToAodAnimation()
         }
 
         setAnimationState(playing = false)
@@ -105,11 +119,24 @@
      */
     fun onScreenTurningOn(onReady: Runnable) {
         if (shouldPlayAnimation) {
+            // The device was not dozing and going to sleep after folding, play the animation
+
             if (isScrimOpaque) {
                 onReady.run()
             } else {
                 pendingScrimReadyCallback = onReady
             }
+        } else if (isFolded && !isFoldHandled && alwaysOnEnabled && isDozing) {
+            // Screen turning on for the first time after folding and we are already dozing
+            // We should play the folding to AOD animation
+
+            setAnimationState(playing = true)
+            statusBar.notificationPanelViewController.prepareFoldToAodAnimation()
+
+            // We don't need to wait for the scrim as it is already displayed
+            // but we should wait for the initial animation preparations to be drawn
+            // (setting initial alpha/translation)
+            OneShotPreDrawListener.add(statusBar.notificationPanelViewController.view, onReady)
         } else {
             // No animation, call ready callback immediately
             onReady.run()
@@ -136,6 +163,10 @@
         }
     }
 
+    fun setIsDozing(dozing: Boolean) {
+        isDozing = dozing
+    }
+
     override fun isAnimationPlaying(): Boolean = isAnimationPlaying
 
     override fun isKeyguardHideDelayed(): Boolean = isAnimationPlaying()
@@ -166,4 +197,15 @@
     interface FoldAodAnimationStatus {
         fun onFoldToAodAnimationChanged()
     }
+
+    private inner class FoldListener :
+        DeviceStateManager.FoldStateListener(
+            context,
+            Consumer { isFolded ->
+                if (!isFolded) {
+                    // We are unfolded now, reset the fold handle status
+                    isFoldHandled = false
+                }
+                this.isFolded = isFolded
+            })
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 6fefce2..b2a79b0 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -21,7 +21,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -97,7 +97,7 @@
         implements CommandQueue.Callbacks, ProtoTraceable<SystemUiTraceProto> {
     private static final String TAG = WMShell.class.getName();
     private static final int INVALID_SYSUI_STATE_MASK =
-            SYSUI_STATE_GLOBAL_ACTIONS_SHOWING
+            SYSUI_STATE_DIALOG_SHOWING
                     | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
                     | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
                     | SYSUI_STATE_BOUNCER_SHOWING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java
index 40632a8..7a0db1f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java
@@ -42,6 +42,7 @@
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.SmartReplyController;
+import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -112,6 +113,11 @@
         // KeyguardUpdateMonitor to be created (injected).
         // TODO(b/1531701009) Clean up NotificationContentView creation to prevent this
         mDependency.injectMockDependency(SmartReplyController.class);
+
+        // Make sure that all tests on any SystemUIDialog does not crash because this dependency
+        // is missing (constructing the actual one would throw).
+        // TODO(b/219008720): Remove this.
+        mDependency.injectMockDependency(SystemUIDialogManager.class);
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 1dd5e22..6e5926d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -88,6 +88,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 
 @LargeTest
 @TestableLooper.RunWithLooper
@@ -345,15 +346,17 @@
 
     @Test
     public void onOrientationChanged_disabled_updateDisplayRotation() {
-        final Display display = Mockito.spy(mContext.getDisplay());
-        when(display.getRotation()).thenReturn(Surface.ROTATION_90);
-        when(mContext.getDisplay()).thenReturn(display);
+        final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+        // Rotate the window clockwise 90 degree.
+        windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
+                windowBounds.right);
+        mWindowManager.setWindowBounds(windowBounds);
+        final int newRotation = simulateRotateTheDevice();
 
-        mInstrumentation.runOnMainSync(() -> {
-            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_ORIENTATION);
-        });
+        mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged(
+                ActivityInfo.CONFIG_ORIENTATION));
 
-        assertEquals(Surface.ROTATION_90, mWindowMagnificationController.mRotation);
+        assertEquals(newRotation, mWindowMagnificationController.mRotation);
     }
 
     @Test
@@ -603,6 +606,113 @@
         ReferenceTestUtils.waitForCondition(() -> hasMagnificationOverlapFlag());
     }
 
+    @Test
+    public void setMinimumWindowSize_enabled_expectedWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int  expectedWindowHeight = minimumWindowSize;
+        final int  expectedWindowWidth = minimumWindowSize;
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight);
+            actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height);
+            actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width);
+
+        });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setMinimumWindowSizeThenEnable_expectedWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int  expectedWindowHeight = minimumWindowSize;
+        final int  expectedWindowWidth = minimumWindowSize;
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight);
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                    Float.NaN, Float.NaN);
+            actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height);
+            actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width);
+        });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowSizeLessThanMin_enabled_minimumWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(minimumWindowSize - 10,
+                    minimumWindowSize - 10);
+            actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height);
+            actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width);
+        });
+
+        assertEquals(minimumWindowSize, actualWindowHeight.get());
+        assertEquals(minimumWindowSize, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowSizeLargerThanScreenSize_enabled_windowSizeIsScreenSize() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(bounds.width() + 10, bounds.height() + 10);
+            actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height);
+            actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width);
+        });
+
+        assertEquals(bounds.height(), actualWindowHeight.get());
+        assertEquals(bounds.width(), actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowCenterOutOfScreen_enabled_magnificationCenterIsInsideTheScreen() {
+
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger magnificationCenterX = new AtomicInteger();
+        final AtomicInteger magnificationCenterY = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSizeAndCenter(minimumWindowSize,
+                    minimumWindowSize, bounds.right, bounds.bottom);
+            magnificationCenterX.set((int) mWindowMagnificationController.getCenterX());
+            magnificationCenterY.set((int) mWindowMagnificationController.getCenterY());
+        });
+
+        assertTrue(magnificationCenterX.get() < bounds.right);
+        assertTrue(magnificationCenterY.get() < bounds.bottom);
+    }
+
     private CharSequence getAccessibilityWindowTitle() {
         final View mirrorView = mWindowManager.getAttachedView();
         if (mirrorView == null) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 7f72dda..6587029 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -29,8 +29,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.battery.BatteryMeterViewController;
-import com.android.systemui.statusbar.policy.BatteryController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,10 +44,6 @@
     @Mock
     DreamOverlayStatusBarView mView;
     @Mock
-    BatteryController mBatteryController;
-    @Mock
-    BatteryMeterViewController mBatteryMeterViewController;
-    @Mock
     ConnectivityManager mConnectivityManager;
     @Mock
     NetworkCapabilities mNetworkCapabilities;
@@ -61,22 +55,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mController = new DreamOverlayStatusBarViewController(
-                mContext, mView, mBatteryController, mBatteryMeterViewController,
-                mConnectivityManager);
-    }
-
-    @Test
-    public void testOnInitInitializesControllers() {
-        mController.onInit();
-        verify(mBatteryMeterViewController).init();
-    }
-
-    @Test
-    public void testOnViewAttachedAddsBatteryControllerCallback() {
-        mController.onViewAttached();
-        verify(mBatteryController)
-                .addCallback(any(BatteryController.BatteryStateChangeCallback.class));
+        mController = new DreamOverlayStatusBarViewController(mView, mConnectivityManager);
     }
 
     @Test
@@ -113,13 +92,6 @@
     }
 
     @Test
-    public void testOnViewDetachedRemovesBatteryControllerCallback() {
-        mController.onViewDetached();
-        verify(mBatteryController)
-                .removeCallback(any(BatteryController.BatteryStateChangeCallback.class));
-    }
-
-    @Test
     public void testOnViewDetachedUnregistersNetworkCallback() {
         mController.onViewDetached();
         verify(mConnectivityManager)
@@ -127,26 +99,6 @@
     }
 
     @Test
-    public void testBatteryPercentTextShownWhenBatteryLevelChangesWhileCharging() {
-        final ArgumentCaptor<BatteryController.BatteryStateChangeCallback> callbackCapture =
-                ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback.class);
-        mController.onViewAttached();
-        verify(mBatteryController).addCallback(callbackCapture.capture());
-        callbackCapture.getValue().onBatteryLevelChanged(1, true, true);
-        verify(mView).showBatteryPercentText(true);
-    }
-
-    @Test
-    public void testBatteryPercentTextHiddenWhenBatteryLevelChangesWhileNotCharging() {
-        final ArgumentCaptor<BatteryController.BatteryStateChangeCallback> callbackCapture =
-                ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback.class);
-        mController.onViewAttached();
-        verify(mBatteryController).addCallback(callbackCapture.capture());
-        callbackCapture.getValue().onBatteryLevelChanged(1, true, false);
-        verify(mView).showBatteryPercentText(false);
-    }
-
-    @Test
     public void testWifiStatusHiddenWhenWifiBecomesAvailable() {
         // Make sure wifi starts out unavailable when onViewAttached is called.
         when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index 71fc8ee..953be7d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -19,7 +19,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
@@ -57,13 +56,11 @@
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.GlobalActions;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.telephony.TelephonyListenerManager;
@@ -112,7 +109,6 @@
     @Mock private UiEventLogger mUiEventLogger;
     @Mock private RingerModeTracker mRingerModeTracker;
     @Mock private RingerModeLiveData mRingerModeLiveData;
-    @Mock private SysUiState mSysUiState;
     @Mock private PackageManager mPackageManager;
     @Mock private Handler mHandler;
     @Mock private UserContextProvider mUserContextProvider;
@@ -120,7 +116,6 @@
     @Mock private StatusBar mStatusBar;
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock private DialogLaunchAnimator mDialogLaunchAnimator;
-    @Mock private SystemUIDialogManager mDialogManager;
 
     private TestableLooper mTestableLooper;
 
@@ -161,19 +156,16 @@
                 mBackgroundExecutor,
                 mUiEventLogger,
                 mRingerModeTracker,
-                mSysUiState,
                 mHandler,
                 mPackageManager,
                 Optional.of(mStatusBar),
                 mKeyguardUpdateMonitor,
-                mDialogLaunchAnimator,
-                mDialogManager);
+                mDialogLaunchAnimator);
         mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting();
 
         ColorExtractor.GradientColors backdropColors = new ColorExtractor.GradientColors();
         backdropColors.setMainColor(Color.BLACK);
         when(mColorExtractor.getNeutralColors()).thenReturn(backdropColors);
-        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index c5c4d79..2be30b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -45,7 +45,6 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -68,7 +67,6 @@
             mock(NotificationEntryManager.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
-    private final SystemUIDialogManager mDialogManager = mock(SystemUIDialogManager.class);
 
     private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
     private MediaOutputController mMediaOutputController;
@@ -82,7 +80,7 @@
     public void setUp() {
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, mDialogManager);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
         mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
                 mMediaOutputController);
         mMediaOutputBaseDialogImpl.onCreate(new Bundle());
@@ -175,7 +173,7 @@
     class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog {
 
         MediaOutputBaseDialogImpl(Context context, MediaOutputController mediaOutputController) {
-            super(context, mediaOutputController, mDialogManager);
+            super(context, mediaOutputController);
 
             mAdapter = mMediaOutputBaseAdapter;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index bdc3117..789822e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -55,7 +55,6 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -94,7 +93,6 @@
     private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
-    private final SystemUIDialogManager mDialogManager = mock(SystemUIDialogManager.class);
 
     private Context mSpyContext;
     private MediaOutputController mMediaOutputController;
@@ -117,7 +115,7 @@
 
         mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, mDialogManager);
+                mNotifCollection, mUiEventLogger, mDialogLaunchAnimator);
         mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         MediaDescription.Builder builder = new MediaDescription.Builder();
@@ -161,7 +159,7 @@
     public void start_withoutPackageName_verifyMediaControllerInit() {
         mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, mDialogManager);
+                mNotifCollection, mUiEventLogger, mDialogLaunchAnimator);
 
         mMediaOutputController.start(mCb);
 
@@ -182,7 +180,7 @@
     public void stop_withoutPackageName_verifyMediaControllerDeinit() {
         mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, mDialogManager);
+                mNotifCollection, mUiEventLogger, mDialogLaunchAnimator);
 
         mMediaOutputController.start(mCb);
 
@@ -453,7 +451,7 @@
     public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
         mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, mDialogManager);
+                mNotifCollection, mUiEventLogger, mDialogLaunchAnimator);
 
         assertThat(mMediaOutputController.getNotificationIcon()).isNull();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index ada8d35..8a3ea56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -40,7 +40,6 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -68,7 +67,6 @@
             mock(NotificationEntryManager.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
-    private final SystemUIDialogManager mDialogManager = mock(SystemUIDialogManager.class);
 
     private MediaOutputDialog mMediaOutputDialog;
     private MediaOutputController mMediaOutputController;
@@ -78,10 +76,10 @@
     public void setUp() {
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, mDialogManager);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         mMediaOutputDialog = new MediaOutputDialog(mContext, false,
-                mMediaOutputController, mUiEventLogger, mDialogManager);
+                mMediaOutputController, mUiEventLogger);
         mMediaOutputDialog.show();
 
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
@@ -127,7 +125,7 @@
     // and verify if the calling times increases.
     public void onCreate_ShouldLogVisibility() {
         MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false,
-                mMediaOutputController, mUiEventLogger, mDialogManager);
+                mMediaOutputController, mUiEventLogger);
         testDialog.show();
 
         testDialog.dismissDialog();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
index b114452..e8cd6c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
@@ -38,7 +38,6 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -67,7 +66,6 @@
             mock(NotificationEntryManager.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
-    private final SystemUIDialogManager mDialogManager = mock(SystemUIDialogManager.class);
 
     private MediaOutputGroupDialog mMediaOutputGroupDialog;
     private MediaOutputController mMediaOutputController;
@@ -77,10 +75,10 @@
     public void setUp() {
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
                 mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
-                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, mDialogManager);
+                mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false,
-                mMediaOutputController, mDialogManager);
+                mMediaOutputController);
         mMediaOutputGroupDialog.show();
         when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
index 24a0ad3..bc54bf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
@@ -18,6 +18,8 @@
 
 import static android.view.DragEvent.ACTION_DRAG_STARTED;
 
+import android.app.Notification;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -36,7 +38,12 @@
 import org.junit.runner.RunWith;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -58,6 +65,7 @@
     private NotificationMenuRow mMenuRow = mock(NotificationMenuRow.class);
     private NotificationMenuRowPlugin.MenuItem mMenuItem =
             mock(NotificationMenuRowPlugin.MenuItem.class);
+    private ShadeController mShadeController = mock(ShadeController.class);
 
     @Before
     public void setUp() throws Exception {
@@ -69,11 +77,15 @@
                 mContext,
                 mDependency,
                 TestableLooper.get(this));
-        mRow = mNotificationTestHelper.createRow();
+        mRow = spy(mNotificationTestHelper.createRow());
+        Notification notification = mRow.getEntry().getSbn().getNotification();
+        notification.contentIntent = mock(PendingIntent.class);
+        doReturn(true).when(mRow).startDragAndDrop(any(), any(), any(), anyInt());
         mGroupRow = mNotificationTestHelper.createGroup(4);
         when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
 
-        mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager);
+        mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager,
+                mShadeController);
     }
 
     @Test
@@ -86,10 +98,6 @@
         mRow.doLongClickCallback(0, 0);
         mRow.doDragCallback(0, 0);
         verify(controller).startDragAndDrop(mRow);
-
-        // Simulate the drag start
-        mRow.dispatchDragEvent(DragEvent.obtain(ACTION_DRAG_STARTED, 0, 0, 0, 0, null, null, null,
-                null, null, false));
         verify(mHeadsUpManager, times(1)).releaseAllImmediately();
     }
 
@@ -98,14 +106,27 @@
         ExpandableNotificationRowDragController controller = createSpyController();
         mRow.setDragController(controller);
 
-        mDependency.get(ShadeController.class).instantExpandNotificationsPanel();
+        mRow.doDragCallback(0, 0);
+        verify(controller).startDragAndDrop(mRow);
+        verify(mShadeController).animateCollapsePanels(eq(0), eq(true),
+                eq(false), anyFloat());
+    }
+
+    @Test
+    public void testDoStartDrag_noLaunchIntent() throws Exception {
+        ExpandableNotificationRowDragController controller = createSpyController();
+        mRow.setDragController(controller);
+
+        // Clear the intents
+        Notification notification = mRow.getEntry().getSbn().getNotification();
+        notification.contentIntent = null;
+        notification.fullScreenIntent = null;
+
         mRow.doDragCallback(0, 0);
         verify(controller).startDragAndDrop(mRow);
 
-        // Simulate the drag start
-        mRow.dispatchDragEvent(DragEvent.obtain(ACTION_DRAG_STARTED, 0, 0, 0, 0, null, null, null,
-                null, null, false));
-        verify(mDependency.get(ShadeController.class)).animateCollapsePanels(0, true);
+        // Verify that we never start the actual drag since there is no content
+        verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt());
     }
 
     private ExpandableNotificationRowDragController createSpyController() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 38d7ce7..6ce3b4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -59,6 +59,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Optional;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -97,7 +98,7 @@
                 mStatusBarStateController, mDeviceProvisionedController, mHeadsUpManager,
                 mBatteryController, mScrimController, () -> mBiometricUnlockController,
                 mKeyguardViewMediator, () -> mAssistManager, mDozeScrimController,
-                mKeyguardUpdateMonitor, mPulseExpansionHandler,
+                mKeyguardUpdateMonitor, mPulseExpansionHandler, Optional.empty(),
                 mNotificationShadeWindowController, mNotificationWakeUpCoordinator,
                 mAuthController, mNotificationIconAreaController);
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 62da981..0e99265 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1363,8 +1363,18 @@
      * </p>
      *
      * @param displayId The logical display id
-     * @param region the new magnified region, may be empty if
-     *               magnification is not enabled (e.g. scale is 1)
+     * @param region The magnification region.
+     *               If the config mode is
+     *               {@link MagnificationConfig#MAGNIFICATION_MODE_FULLSCREEN},
+     *               it is the region of the screen currently active for magnification.
+     *               the returned region will be empty if the magnification is not active
+     *               (e.g. scale is 1. And the magnification is active if magnification
+     *               gestures are enabled or if a service is running that can control
+     *               magnification.
+     *               If the config mode is
+     *               {@link MagnificationConfig#MAGNIFICATION_MODE_WINDOW},
+     *               it is the region of screen projected on the magnification window.
+     *               The region will be empty if magnification is not activated.
      * @param config The magnification config. That has magnification mode, the new scale and the
      *              new screen-relative center position
      */
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index fe97a46..a958209 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -50,7 +50,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
-import com.android.server.accessibility.AccessibilityManagerService;
 import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.wm.WindowManagerInternal;
 
@@ -374,9 +373,8 @@
                     .setScale(getScale())
                     .setCenterX(getCenterX())
                     .setCenterY(getCenterY()).build();
-            mControllerCtx.getAms().notifyMagnificationChanged(mDisplayId,
-                    mMagnificationRegion,
-                    config);
+            mMagnificationInfoChangedCallback.onFullScreenMagnificationChanged(mDisplayId,
+                    mMagnificationRegion, config);
             if (mUnregisterPending && !isMagnifying()) {
                 unregister(mDeleteAfterUnregister);
             }
@@ -665,10 +663,10 @@
      * FullScreenMagnificationController Constructor
      */
     public FullScreenMagnificationController(@NonNull Context context,
-            @NonNull AccessibilityManagerService ams, @NonNull Object lock,
+            @NonNull AccessibilityTraceManager traceManager, @NonNull Object lock,
             @NonNull MagnificationInfoChangedCallback magnificationInfoChangedCallback,
             @NonNull MagnificationScaleProvider scaleProvider) {
-        this(new ControllerContext(context, ams,
+        this(new ControllerContext(context, traceManager,
                 LocalServices.getService(WindowManagerInternal.class),
                 new Handler(context.getMainLooper()),
                 context.getResources().getInteger(R.integer.config_longAnimTime)), lock,
@@ -1521,7 +1519,6 @@
     @VisibleForTesting
     public static class ControllerContext {
         private final Context mContext;
-        private final AccessibilityManagerService mAms;
         private final AccessibilityTraceManager mTrace;
         private final WindowManagerInternal mWindowManager;
         private final Handler mHandler;
@@ -1531,13 +1528,12 @@
          * Constructor for ControllerContext.
          */
         public ControllerContext(@NonNull Context context,
-                @NonNull AccessibilityManagerService ams,
+                @NonNull AccessibilityTraceManager traceManager,
                 @NonNull WindowManagerInternal windowManager,
                 @NonNull Handler handler,
                 long animationDuration) {
             mContext = context;
-            mAms = ams;
-            mTrace = ams.getTraceManager();
+            mTrace = traceManager;
             mWindowManager = windowManager;
             mHandler = handler;
             mAnimationDuration = animationDuration;
@@ -1552,14 +1548,6 @@
         }
 
         /**
-         * @return AccessibilityManagerService
-         */
-        @NonNull
-        public AccessibilityManagerService getAms() {
-            return mAms;
-        }
-
-        /**
          * @return AccessibilityTraceManager
          */
         @NonNull
@@ -1632,5 +1620,17 @@
          *                           hidden.
          */
         void onImeWindowVisibilityChanged(boolean shown);
+
+        /**
+         * Called when the magnification spec changed.
+         *
+         * @param displayId The logical display id
+         * @param region    The region of the screen currently active for magnification.
+         *                  The returned region will be empty if the magnification is not active.
+         * @param config    The magnification config. That has magnification mode, the new scale and
+         *                  the new screen-relative center position
+         */
+        void onFullScreenMagnificationChanged(int displayId, @NonNull Region region,
+                @NonNull MagnificationConfig config);
     }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index c376bf8..09e82c7 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -405,6 +405,12 @@
         mAms.notifyMagnificationChanged(displayId, new Region(bounds), config);
     }
 
+    @Override
+    public void onFullScreenMagnificationChanged(int displayId, @NonNull Region region,
+            @NonNull MagnificationConfig config) {
+        mAms.notifyMagnificationChanged(displayId, region, config);
+    }
+
     private void disableFullScreenMagnificationIfNeeded(int displayId) {
         final FullScreenMagnificationController fullScreenMagnificationController =
                 getFullScreenMagnificationController();
@@ -590,7 +596,7 @@
         synchronized (mLock) {
             if (mFullScreenMagnificationController == null) {
                 mFullScreenMagnificationController = new FullScreenMagnificationController(mContext,
-                        mAms, mLock, this, mScaleProvider);
+                        mAms.getTraceManager(), mLock, this, mScaleProvider);
             }
         }
         return mFullScreenMagnificationController;
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
index 8963337..7e3ede1 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
+++ b/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
@@ -22,20 +22,18 @@
 import android.app.backup.RestoreSet;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
-import android.os.Binder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.util.Slog;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.backup.IBackupTransport;
 import com.android.internal.infra.AndroidFuture;
 
 import java.util.ArrayDeque;
-import java.util.Deque;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Queue;
-import java.util.concurrent.ArrayBlockingQueue;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -49,17 +47,19 @@
 
     private final IBackupTransport mTransportBinder;
     private final TransportStatusCallbackPool mCallbackPool;
+    private final TransportFutures mTransportFutures;
 
     BackupTransportClient(IBackupTransport transportBinder) {
         mTransportBinder = transportBinder;
         mCallbackPool = new TransportStatusCallbackPool();
+        mTransportFutures = new TransportFutures();
     }
 
     /**
      * See {@link IBackupTransport#name()}.
      */
     public String name() throws RemoteException {
-        AndroidFuture<String> resultFuture = new AndroidFuture<>();
+        AndroidFuture<String> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.name(resultFuture);
         return getFutureResult(resultFuture);
     }
@@ -68,7 +68,7 @@
      * See {@link IBackupTransport#configurationIntent()}
      */
     public Intent configurationIntent() throws RemoteException {
-        AndroidFuture<Intent> resultFuture = new AndroidFuture<>();
+        AndroidFuture<Intent> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.configurationIntent(resultFuture);
         return getFutureResult(resultFuture);
     }
@@ -77,7 +77,7 @@
      * See {@link IBackupTransport#currentDestinationString()}
      */
     public String currentDestinationString() throws RemoteException {
-        AndroidFuture<String> resultFuture = new AndroidFuture<>();
+        AndroidFuture<String> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.currentDestinationString(resultFuture);
         return getFutureResult(resultFuture);
     }
@@ -86,7 +86,7 @@
      * See {@link IBackupTransport#dataManagementIntent()}
      */
     public Intent dataManagementIntent() throws RemoteException {
-        AndroidFuture<Intent> resultFuture = new AndroidFuture<>();
+        AndroidFuture<Intent> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.dataManagementIntent(resultFuture);
         return getFutureResult(resultFuture);
     }
@@ -96,7 +96,7 @@
      */
     @Nullable
     public CharSequence dataManagementIntentLabel() throws RemoteException {
-        AndroidFuture<CharSequence> resultFuture = new AndroidFuture<>();
+        AndroidFuture<CharSequence> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.dataManagementIntentLabel(resultFuture);
         return getFutureResult(resultFuture);
     }
@@ -105,7 +105,7 @@
      * See {@link IBackupTransport#transportDirName()}
      */
     public String transportDirName() throws RemoteException {
-        AndroidFuture<String> resultFuture = new AndroidFuture<>();
+        AndroidFuture<String> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.transportDirName(resultFuture);
         return getFutureResult(resultFuture);
     }
@@ -153,7 +153,7 @@
      * See {@link IBackupTransport#requestBackupTime()}
      */
     public long requestBackupTime() throws RemoteException {
-        AndroidFuture<Long> resultFuture = new AndroidFuture<>();
+        AndroidFuture<Long> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.requestBackupTime(resultFuture);
         Long result = getFutureResult(resultFuture);
         return result == null ? BackupTransport.TRANSPORT_ERROR : result;
@@ -177,7 +177,7 @@
      * See {@link IBackupTransport#getAvailableRestoreSets()}
      */
     public RestoreSet[] getAvailableRestoreSets() throws RemoteException {
-        AndroidFuture<List<RestoreSet>> resultFuture = new AndroidFuture<>();
+        AndroidFuture<List<RestoreSet>> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.getAvailableRestoreSets(resultFuture);
         List<RestoreSet> result = getFutureResult(resultFuture);
         return result == null ? null : result.toArray(new RestoreSet[] {});
@@ -187,7 +187,7 @@
      * See {@link IBackupTransport#getCurrentRestoreSet()}
      */
     public long getCurrentRestoreSet() throws RemoteException {
-        AndroidFuture<Long> resultFuture = new AndroidFuture<>();
+        AndroidFuture<Long> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.getCurrentRestoreSet(resultFuture);
         Long result = getFutureResult(resultFuture);
         return result == null ? BackupTransport.TRANSPORT_ERROR : result;
@@ -210,7 +210,7 @@
      * See {@link IBackupTransport#nextRestorePackage()}
      */
     public RestoreDescription nextRestorePackage() throws RemoteException {
-        AndroidFuture<RestoreDescription> resultFuture = new AndroidFuture<>();
+        AndroidFuture<RestoreDescription> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.nextRestorePackage(resultFuture);
         return getFutureResult(resultFuture);
     }
@@ -245,7 +245,7 @@
      * See {@link IBackupTransport#requestFullBackupTime()}
      */
     public long requestFullBackupTime() throws RemoteException {
-        AndroidFuture<Long> resultFuture = new AndroidFuture<>();
+        AndroidFuture<Long> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.requestFullBackupTime(resultFuture);
         Long result = getFutureResult(resultFuture);
         return result == null ? BackupTransport.TRANSPORT_ERROR : result;
@@ -309,7 +309,7 @@
      */
     public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup)
             throws RemoteException {
-        AndroidFuture<Boolean> resultFuture = new AndroidFuture<>();
+        AndroidFuture<Boolean> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.isAppEligibleForBackup(targetPackage, isFullBackup, resultFuture);
         Boolean result = getFutureResult(resultFuture);
         return result != null && result;
@@ -319,7 +319,7 @@
      * See {@link IBackupTransport#getBackupQuota(String, boolean)}
      */
     public long getBackupQuota(String packageName, boolean isFullBackup) throws RemoteException {
-        AndroidFuture<Long> resultFuture = new AndroidFuture<>();
+        AndroidFuture<Long> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.getBackupQuota(packageName, isFullBackup, resultFuture);
         Long result = getFutureResult(resultFuture);
         return result == null ? BackupTransport.TRANSPORT_ERROR : result;
@@ -355,18 +355,58 @@
      * See {@link IBackupTransport#getTransportFlags()}
      */
     public int getTransportFlags() throws RemoteException {
-        AndroidFuture<Integer> resultFuture = new AndroidFuture<>();
+        AndroidFuture<Integer> resultFuture = mTransportFutures.newFuture();
         mTransportBinder.getTransportFlags(resultFuture);
         Integer result = getFutureResult(resultFuture);
         return result == null ? BackupTransport.TRANSPORT_ERROR : result;
     }
 
+    /**
+     * Allows the {@link TransportConnection} to notify this client
+     * if the underlying transport has become unusable.  If that happens
+     * we want to cancel all active futures or callbacks.
+     */
+    void onBecomingUnusable() {
+        mCallbackPool.cancelActiveCallbacks();
+        mTransportFutures.cancelActiveFutures();
+    }
+
     private <T> T getFutureResult(AndroidFuture<T> future) {
         try {
             return future.get(600, TimeUnit.SECONDS);
         } catch (InterruptedException | ExecutionException | TimeoutException e) {
             Slog.w(TAG, "Failed to get result from transport:", e);
             return null;
+        } finally {
+            mTransportFutures.remove(future);
+        }
+    }
+
+    private static class TransportFutures {
+        private final Object mActiveFuturesLock = new Object();
+        private final Set<AndroidFuture<?>> mActiveFutures = new HashSet<>();
+
+        <T> AndroidFuture<T> newFuture() {
+            AndroidFuture<T> future = new AndroidFuture<>();
+            synchronized (mActiveFuturesLock) {
+                mActiveFutures.add(future);
+            }
+            return future;
+        }
+
+        <T> void remove(AndroidFuture<T> future) {
+            synchronized (mActiveFuturesLock) {
+                mActiveFutures.remove(future);
+            }
+        }
+
+        void cancelActiveFutures() {
+            synchronized (mActiveFuturesLock) {
+                for (AndroidFuture<?> future : mActiveFutures) {
+                    future.cancel(true);
+                }
+                mActiveFutures.clear();
+            }
         }
     }
 
@@ -375,27 +415,47 @@
 
         private final Object mPoolLock = new Object();
         private final Queue<TransportStatusCallback> mCallbackPool = new ArrayDeque<>();
+        private final Set<TransportStatusCallback> mActiveCallbacks = new HashSet<>();
 
         TransportStatusCallback acquire() {
             synchronized (mPoolLock) {
-                if (mCallbackPool.isEmpty()) {
-                    return new TransportStatusCallback();
-                } else {
-                    return mCallbackPool.poll();
+                TransportStatusCallback callback = mCallbackPool.poll();
+                if (callback == null) {
+                    callback = new TransportStatusCallback();
                 }
+                callback.reset();
+                mActiveCallbacks.add(callback);
+                return callback;
             }
         }
 
         void recycle(TransportStatusCallback callback) {
             synchronized (mPoolLock) {
+                mActiveCallbacks.remove(callback);
                 if (mCallbackPool.size() > MAX_POOL_SIZE) {
                     Slog.d(TAG, "TransportStatusCallback pool size exceeded");
                     return;
                 }
-
-                callback.reset();
                 mCallbackPool.add(callback);
             }
         }
+
+        void cancelActiveCallbacks() {
+            synchronized (mPoolLock) {
+                for (TransportStatusCallback callback : mActiveCallbacks) {
+                    try {
+                        callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+                        // This waits for status to propagate before the callback is reset.
+                        callback.getOperationStatus();
+                    } catch (RemoteException ex) {
+                        // Nothing we can do.
+                    }
+                    if (mCallbackPool.size() < MAX_POOL_SIZE) {
+                        mCallbackPool.add(callback);
+                    }
+                }
+                mActiveCallbacks.clear();
+            }
+        }
     }
 }
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java b/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java
index f9a3c36..1009787 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java
+++ b/services/backup/backuplib/java/com/android/server/backup/transport/TransportConnection.java
@@ -449,6 +449,9 @@
     private void onServiceDisconnected() {
         synchronized (mStateLock) {
             log(Priority.ERROR, "Service disconnected: client UNUSABLE");
+            if (mTransport != null) {
+                mTransport.onBecomingUnusable();
+            }
             setStateLocked(State.UNUSABLE, null);
             try {
                 // After unbindService() no calls back to mConnection
@@ -473,6 +476,9 @@
             checkStateIntegrityLocked();
 
             log(Priority.ERROR, "Binding died: client UNUSABLE");
+            if (mTransport != null) {
+                mTransport.onBecomingUnusable();
+            }
             // After unbindService() no calls back to mConnection
             switch (mState) {
                 case State.UNUSABLE:
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java b/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java
index a55178c..bc5cb02 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java
+++ b/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java
@@ -75,13 +75,11 @@
             }
 
             Slog.w(TAG, "Couldn't get operation status from transport");
-            return BackupTransport.TRANSPORT_ERROR;
         } catch (InterruptedException e) {
             Slog.w(TAG, "Couldn't get operation status from transport: ", e);
-            return BackupTransport.TRANSPORT_ERROR;
-        } finally {
-            reset();
         }
+
+        return BackupTransport.TRANSPORT_ERROR;
     }
 
     synchronized void reset() {
diff --git a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
index 21a677b8..cb28254 100644
--- a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
+++ b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
@@ -171,12 +171,20 @@
         broadcastChange(CHANGE_TYPE_REMOVED, association);
     }
 
+    /**
+     * @return a "snapshot" of the current state of the existing associations.
+     */
     public @NonNull Collection<AssociationInfo> getAssociations() {
-        final Collection<AssociationInfo> allAssociations;
         synchronized (mLock) {
-            allAssociations = mIdMap.values();
+            // IMPORTANT: make and return a COPY of the mIdMap.values(), NOT a "direct" reference.
+            // The HashMap.values() returns a collection which is backed by the HashMap, so changes
+            // to the HashMap are reflected in this collection.
+            // For us this means that if mIdMap is modified while the iteration over mIdMap.values()
+            // is in progress it may lead to "undefined results" (according to the HashMap's
+            // documentation) or cause ConcurrentModificationExceptions in the iterator (according
+            // to the bugreports...).
+            return List.copyOf(mIdMap.values());
         }
-        return Collections.unmodifiableCollection(allAssociations);
     }
 
     public @NonNull List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 6813f3f8..1c10304 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -15623,24 +15623,20 @@
         }
         for (int i = 0, size = processes.size(); i < size; i++) {
             ProcessRecord app = processes.get(i);
-            pw.println(String.format("------ DUMP RESOURCES %s (%s)  ------",
+            pw.println(String.format("Resources History for %s (%s)",
                     app.processName,
                     app.info.packageName));
             pw.flush();
             try {
-                TransferPipe tp = new TransferPipe();
+                TransferPipe tp = new TransferPipe("  ");
                 try {
                     IApplicationThread thread = app.getThread();
                     if (thread != null) {
                         app.getThread().dumpResources(tp.getWriteFd(), null);
                         tp.go(fd.getFileDescriptor(), 2000);
-                        pw.println(String.format("------ END DUMP RESOURCES %s (%s)  ------",
-                                app.processName,
-                                app.info.packageName));
-                        pw.flush();
                     } else {
                         pw.println(String.format(
-                                "------ DUMP RESOURCES %s (%s) failed, no thread ------",
+                                "  Resources history for %s (%s) failed, no thread",
                                 app.processName,
                                 app.info.packageName));
                     }
@@ -15648,11 +15644,7 @@
                     tp.kill();
                 }
             } catch (IOException e) {
-                pw.println(String.format(
-                        "------ EXCEPTION DUMPING RESOURCES for %s (%s): %s ------",
-                        app.processName,
-                        app.info.packageName,
-                        e.getMessage()));
+                pw.println("  " + e.getMessage());
                 pw.flush();
             }
 
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 3491cd5..49a935e 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -479,6 +479,8 @@
         }
         if (profile == BluetoothProfile.A2DP) {
             mA2dp = (BluetoothA2dp) proxy;
+        } else if (profile == BluetoothProfile.HEARING_AID) {
+            mHearingAid = (BluetoothHearingAid) proxy;
         } else if (profile == BluetoothProfile.LE_AUDIO) {
             mLeAudio = (BluetoothLeAudio) proxy;
         }
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index eb2f80b..9f46bd6 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -389,7 +389,7 @@
             final long oldIdentity = Binder.clearCallingIdentity();
             try {
                 if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CLIPBOARD,
-                        PROPERTY_AUTO_CLEAR_ENABLED, false)) {
+                        PROPERTY_AUTO_CLEAR_ENABLED, true)) {
                     mClipboardClearHandler.removeEqualMessages(ClipboardClearHandler.MSG_CLEAR,
                             userId);
                     Message clearMessage = Message.obtain(mClipboardClearHandler,
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index ea054a5..682f0df 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -2020,7 +2020,8 @@
                             .setCategory(Notification.CATEGORY_SYSTEM)
                             .setVisibility(Notification.VISIBILITY_PUBLIC)
                             .setOngoing(true)
-                            .setColor(mContext.getColor(R.color.system_notification_accent_color));
+                            .setColor(mContext.getColor(
+                                    android.R.color.system_notification_accent_color));
             notificationManager.notify(TAG, SystemMessage.NOTE_VPN_DISCONNECTED, builder.build());
         } finally {
             Binder.restoreCallingIdentity(token);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index b2f500a..d2d80ff 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -408,6 +408,11 @@
     @GuardedBy("ImfLock.class")
     @NonNull
     InputBindResult bindCurrentMethod() {
+        if (mSelectedMethodId == null) {
+            Slog.e(TAG, "mSelectedMethodId is null!");
+            return InputBindResult.NO_IME;
+        }
+
         InputMethodInfo info = mMethodMap.get(mSelectedMethodId);
         if (info == null) {
             throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId);
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index 0c3f9f0..45d9822 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -1428,6 +1428,7 @@
         ipw.println("Location Settings:");
         ipw.increaseIndent();
         mInjector.getSettingsHelper().dump(fd, ipw, args);
+        mInjector.getLocationSettings().dump(fd, ipw, args);
         ipw.decreaseIndent();
 
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java b/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java
index 8fdde24..e9bf90f 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java
@@ -16,8 +16,6 @@
 
 package com.android.server.location.contexthub;
 
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-
 import android.Manifest;
 import android.content.Context;
 import android.hardware.contexthub.V1_0.AsyncEventType;
@@ -297,19 +295,14 @@
     }
 
     /**
-     * Checks for location hardware permissions.
+     * Checks for ACCESS_CONTEXT_HUB permissions.
      *
      * @param context the context of the service
      */
     /* package */
     static void checkPermissions(Context context) {
-        boolean hasAccessContextHubPermission = (context.checkCallingPermission(
-                CONTEXT_HUB_PERMISSION) == PERMISSION_GRANTED);
-
-        if (!hasAccessContextHubPermission) {
-            throw new SecurityException(
-                    "ACCESS_CONTEXT_HUB permission required to use Context Hub");
-        }
+        context.enforceCallingOrSelfPermission(CONTEXT_HUB_PERMISSION,
+                "ACCESS_CONTEXT_HUB permission required to use Context Hub");
     }
 
     /**
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index d42e2c6..0b8f94c 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -1607,6 +1607,8 @@
 
     public @Nullable Location getLastLocation(LastLocationRequest request,
             CallerIdentity identity, @PermissionLevel int permissionLevel) {
+        request = calculateLastLocationRequest(request);
+
         if (!isActive(request.isBypass(), identity)) {
             return null;
         }
@@ -1634,6 +1636,38 @@
         return location;
     }
 
+    private LastLocationRequest calculateLastLocationRequest(LastLocationRequest baseRequest) {
+        LastLocationRequest.Builder builder = new LastLocationRequest.Builder(baseRequest);
+
+        boolean locationSettingsIgnored = baseRequest.isLocationSettingsIgnored();
+        if (locationSettingsIgnored) {
+            // if we are not currently allowed use location settings ignored, disable it
+            if (!mSettingsHelper.getIgnoreSettingsAllowlist().contains(
+                    getIdentity().getPackageName(), getIdentity().getAttributionTag())
+                    && !mLocationManagerInternal.isProvider(null, getIdentity())) {
+                locationSettingsIgnored = false;
+            }
+
+            builder.setLocationSettingsIgnored(locationSettingsIgnored);
+        }
+
+        boolean adasGnssBypass = baseRequest.isAdasGnssBypass();
+        if (adasGnssBypass) {
+            // if we are not currently allowed use adas gnss bypass, disable it
+            if (!GPS_PROVIDER.equals(mName)) {
+                Log.e(TAG, "adas gnss bypass request received in non-gps provider");
+                adasGnssBypass = false;
+            } else if (!mLocationSettings.getUserSettings(
+                    getIdentity().getUserId()).isAdasGnssLocationEnabled()) {
+                adasGnssBypass = false;
+            }
+
+            builder.setAdasGnssBypass(adasGnssBypass);
+        }
+
+        return builder.build();
+    }
+
     /**
      * This function does not perform any permissions or safety checks, by calling it you are
      * committing to performing all applicable checks yourself. This always returns a "fine"
diff --git a/services/core/java/com/android/server/location/settings/LocationSettings.java b/services/core/java/com/android/server/location/settings/LocationSettings.java
index d521538..be0e7ac 100644
--- a/services/core/java/com/android/server/location/settings/LocationSettings.java
+++ b/services/core/java/com/android/server/location/settings/LocationSettings.java
@@ -18,8 +18,11 @@
 
 import static android.content.pm.PackageManager.FEATURE_AUTOMOTIVE;
 
+import android.app.ActivityManager;
 import android.content.Context;
 import android.os.Environment;
+import android.os.RemoteException;
+import android.util.IndentingPrintWriter;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
@@ -29,6 +32,7 @@
 import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.IOException;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Function;
@@ -104,6 +108,33 @@
         getUserSettingsStore(userId).update(updater);
     }
 
+    /** Dumps info for debugging. */
+    public final void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
+        int[] userIds;
+        try {
+            userIds = ActivityManager.getService().getRunningUserIds();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        if (mContext.getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE)) {
+            ipw.print("ADAS Location Setting: ");
+            ipw.increaseIndent();
+            if (userIds.length > 1) {
+                ipw.println();
+                for (int userId : userIds) {
+                    ipw.print("[u");
+                    ipw.print(userId);
+                    ipw.print("] ");
+                    ipw.println(getUserSettings(userId).isAdasGnssLocationEnabled());
+                }
+            } else {
+                ipw.println(getUserSettings(userIds[0]).isAdasGnssLocationEnabled());
+            }
+            ipw.decreaseIndent();
+        }
+    }
+
     @VisibleForTesting
     final void flushFiles() throws InterruptedException {
         synchronized (mUserSettings) {
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 96391ac..074d891 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -647,18 +647,17 @@
             if (mDestroyed) {
                 return;
             }
-            toSend = new ArrayList<>();
-            if (mQueue != null) {
-                toSend.ensureCapacity(mQueue.size());
-                toSend.addAll(mQueue);
-            }
+            toSend = mQueue == null ? null : new ArrayList<>(mQueue);
         }
         Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null;
         for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
-            ParceledListSlice<QueueItem> parcelableQueue = new ParceledListSlice<>(toSend);
-            // Limit the size of initial Parcel to prevent binder buffer overflow
-            // as onQueueChanged is an async binder call.
-            parcelableQueue.setInlineCountLimit(1);
+            ParceledListSlice<QueueItem> parcelableQueue = null;
+            if (toSend != null) {
+                parcelableQueue = new ParceledListSlice<>(toSend);
+                // Limit the size of initial Parcel to prevent binder buffer overflow
+                // as onQueueChanged is an async binder call.
+                parcelableQueue.setInlineCountLimit(1);
+            }
 
             try {
                 holder.mCallback.onQueueChanged(parcelableQueue);
diff --git a/services/core/java/com/android/server/net/LockdownVpnTracker.java b/services/core/java/com/android/server/net/LockdownVpnTracker.java
index 851ea3d..1b7d1ba 100644
--- a/services/core/java/com/android/server/net/LockdownVpnTracker.java
+++ b/services/core/java/com/android/server/net/LockdownVpnTracker.java
@@ -293,7 +293,7 @@
                         .addAction(R.drawable.ic_menu_refresh, mContext.getString(R.string.reset),
                                 mResetIntent)
                         .setColor(mContext.getColor(
-                                com.android.internal.R.color.system_notification_accent_color));
+                                android.R.color.system_notification_accent_color));
 
         mNotificationManager.notify(null /* tag */, SystemMessage.NOTE_VPN_STATUS,
                 builder.build());
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 0052df3..60962b1 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -4157,7 +4157,7 @@
         if (mRestrictedNetworkingMode) {
             // Note: setUidFirewallRule also updates mUidFirewallRestrictedModeRules.
             // In this case, default firewall rules can also be added.
-            setUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, uid,
+            setUidFirewallRuleUL(FIREWALL_CHAIN_RESTRICTED, uid,
                     getRestrictedModeFirewallRule(uidBlockedState));
         }
     }
@@ -4321,10 +4321,10 @@
                 && (uidBlockedState.effectiveBlockedReasons & BLOCKED_REASON_LOW_POWER_STANDBY)
                 == 0) {
             mUidFirewallLowPowerStandbyModeRules.put(uid, FIREWALL_RULE_ALLOW);
-            setUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, uid, FIREWALL_RULE_ALLOW);
+            setUidFirewallRuleUL(FIREWALL_CHAIN_LOW_POWER_STANDBY, uid, FIREWALL_RULE_ALLOW);
         } else {
             mUidFirewallLowPowerStandbyModeRules.delete(uid);
-            setUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, uid, FIREWALL_RULE_DEFAULT);
+            setUidFirewallRuleUL(FIREWALL_CHAIN_LOW_POWER_STANDBY, uid, FIREWALL_RULE_DEFAULT);
         }
     }
 
@@ -4373,9 +4373,9 @@
             final boolean isWhitelisted = isWhitelistedFromPowerSaveUL(uid,
                     chain == FIREWALL_CHAIN_DOZABLE);
             if (isWhitelisted || isUidForegroundOnRestrictPowerUL(uid)) {
-                setUidFirewallRule(chain, uid, FIREWALL_RULE_ALLOW);
+                setUidFirewallRuleUL(chain, uid, FIREWALL_RULE_ALLOW);
             } else {
-                setUidFirewallRule(chain, uid, FIREWALL_RULE_DEFAULT);
+                setUidFirewallRuleUL(chain, uid, FIREWALL_RULE_DEFAULT);
             }
         }
     }
@@ -4421,10 +4421,10 @@
             int appId = UserHandle.getAppId(uid);
             if (!mPowerSaveTempWhitelistAppIds.get(appId) && isUidIdle(uid)
                     && !isUidForegroundOnRestrictPowerUL(uid)) {
-                setUidFirewallRule(FIREWALL_CHAIN_STANDBY, uid, FIREWALL_RULE_DENY);
+                setUidFirewallRuleUL(FIREWALL_CHAIN_STANDBY, uid, FIREWALL_RULE_DENY);
                 if (LOGD) Log.d(TAG, "updateRuleForAppIdleUL DENY " + uid);
             } else {
-                setUidFirewallRule(FIREWALL_CHAIN_STANDBY, uid, FIREWALL_RULE_DEFAULT);
+                setUidFirewallRuleUL(FIREWALL_CHAIN_STANDBY, uid, FIREWALL_RULE_DEFAULT);
                 if (LOGD) Log.d(TAG, "updateRuleForAppIdleUL " + uid + " to DEFAULT");
             }
         } finally {
@@ -5495,10 +5495,11 @@
     /**
      * Add or remove a uid to the firewall denylist for all network ifaces.
      */
-    private void setUidFirewallRule(int chain, int uid, int rule) {
+    @GuardedBy("mUidRulesFirstLock")
+    private void setUidFirewallRuleUL(int chain, int uid, int rule) {
         if (Trace.isTagEnabled(Trace.TRACE_TAG_NETWORK)) {
             Trace.traceBegin(Trace.TRACE_TAG_NETWORK,
-                    "setUidFirewallRule: " + chain + "/" + uid + "/" + rule);
+                    "setUidFirewallRuleUL: " + chain + "/" + uid + "/" + rule);
         }
         try {
             if (chain == FIREWALL_CHAIN_DOZABLE) {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index c09c904..db0b0c58 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -183,7 +183,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 
@@ -1596,18 +1595,16 @@
                         parsedPackage.setRestrictUpdateHash(oldPackage.getRestrictUpdateHash());
                     }
 
-                    // Check for shared user id changes
-                    if (!Objects.equals(oldPackage.getSharedUserId(),
-                            parsedPackage.getSharedUserId())
-                            // Don't mark as invalid if the app is trying to
-                            // leave a sharedUserId
-                            && parsedPackage.getSharedUserId() != null) {
+                    // APK should not change its sharedUserId declarations
+                    final var oldSharedUid = oldPackage.getSharedUserId() != null
+                            ? oldPackage.getSharedUserId() : "<nothing>";
+                    final var newSharedUid = parsedPackage.getSharedUserId() != null
+                            ? parsedPackage.getSharedUserId() : "<nothing>";
+                    if (!oldSharedUid.equals(newSharedUid)) {
                         throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
                                 "Package " + parsedPackage.getPackageName()
                                         + " shared user changed from "
-                                        + (oldPackage.getSharedUserId() != null
-                                        ? oldPackage.getSharedUserId() : "<nothing>")
-                                        + " to " + parsedPackage.getSharedUserId());
+                                        + oldSharedUid + " to " + newSharedUid);
                     }
 
                     // In case of rollback, remember per-user/profile install state
@@ -3696,10 +3693,13 @@
             }
             disabledPkgSetting = mPm.mSettings.getDisabledSystemPkgLPr(
                     parsedPackage.getPackageName());
-            sharedUserSetting = (parsedPackage.getSharedUserId() != null)
-                    ? mPm.mSettings.getSharedUserLPw(parsedPackage.getSharedUserId(),
-                    0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true)
-                    : null;
+            if (parsedPackage.getSharedUserId() != null && !parsedPackage.isLeavingSharedUid()) {
+                sharedUserSetting = mPm.mSettings.getSharedUserLPw(
+                        parsedPackage.getSharedUserId(),
+                        0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true /*create*/);
+            } else {
+                sharedUserSetting = null;
+            }
             if (DEBUG_PACKAGE_SCANNING
                     && (parseFlags & ParsingPackageUtils.PARSE_CHATTY) != 0
                     && sharedUserSetting != null) {
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
index cdc2b12..f6f9faf 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
@@ -290,6 +290,9 @@
     /** @see R#styleable.AndroidManifest_inheritKeyStoreKeys */
     ParsingPackage setInheritKeyStoreKeys(boolean inheritKeyStoreKeys);
 
+    /** @see R#styleable.AndroidManifest_sharedUserMaxSdkVersion */
+    ParsingPackage setLeavingSharedUid(boolean leavingSharedUid);
+
     ParsingPackage setLabelRes(int labelRes);
 
     ParsingPackage setLargestWidthLimitDp(int largestWidthLimitDp);
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageImpl.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageImpl.java
index 177eaca..6767027 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageImpl.java
@@ -549,6 +549,7 @@
         private static final long SDK_LIBRARY = 1L << 49;
         private static final long INHERIT_KEYSTORE_KEYS = 1L << 50;
         private static final long ENABLE_ON_BACK_INVOKED_CALLBACK = 1L << 51;
+        private static final long LEAVING_SHARED_UID = 1L << 52;
     }
 
     private ParsingPackageImpl setBoolean(@Booleans.Values long flag, boolean value) {
@@ -2403,6 +2404,11 @@
     }
 
     @Override
+    public boolean isLeavingSharedUid() {
+        return getBoolean(Booleans.LEAVING_SHARED_UID);
+    }
+
+    @Override
     public ParsingPackageImpl setBaseRevisionCode(int value) {
         baseRevisionCode = value;
         return this;
@@ -2551,6 +2557,11 @@
     }
 
     @Override
+    public ParsingPackageImpl setLeavingSharedUid(boolean value) {
+        return setBoolean(Booleans.LEAVING_SHARED_UID, value);
+    }
+
+    @Override
     public ParsingPackageImpl setLabelRes(int value) {
         labelRes = value;
         return this;
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageRead.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageRead.java
index 428374f..50033f6 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageRead.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageRead.java
@@ -360,4 +360,11 @@
      * @see R.styleable.AndroidManifestApplication_enableOnBackInvokedCallback
      */
     boolean isOnBackInvokedCallbackEnabled();
+
+    /**
+     * Returns true if R.styleable#AndroidManifest_sharedUserMaxSdkVersion is set to a value
+     * smaller than the current SDK version.
+     * @see R.styleable#AndroidManifest_sharedUserMaxSdkVersion
+     */
+    boolean isLeavingSharedUid();
 }
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index f30daa9..ed1ab01 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -1032,11 +1032,6 @@
 
     private static ParseResult<ParsingPackage> parseSharedUser(ParseInput input,
             ParsingPackage pkg, TypedArray sa) {
-        int maxSdkVersion = anInteger(0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa);
-        if ((maxSdkVersion != 0) && maxSdkVersion < Build.VERSION.RESOURCES_SDK_INT) {
-            return input.success(pkg);
-        }
-
         String str = nonConfigString(0, R.styleable.AndroidManifest_sharedUserId, sa);
         if (TextUtils.isEmpty(str)) {
             return input.success(pkg);
@@ -1052,7 +1047,11 @@
             }
         }
 
+        int maxSdkVersion = anInteger(0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa);
+        boolean leaving = (maxSdkVersion != 0) && (maxSdkVersion < Build.VERSION.RESOURCES_SDK_INT);
+
         return input.success(pkg
+                .setLeavingSharedUid(leaving)
                 .setSharedUserId(str.intern())
                 .setSharedUserLabel(resId(R.styleable.AndroidManifest_sharedUserLabel, sa)));
     }
diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
new file mode 100644
index 0000000..3550bda
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java
@@ -0,0 +1,148 @@
+/*
+ * 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.vibrator;
+
+import android.os.SystemClock;
+import android.os.VibrationEffect;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represent a step on a single vibrator that plays one or more segments from a
+ * {@link VibrationEffect.Composed} effect.
+ */
+abstract class AbstractVibratorStep extends Step {
+    public final VibratorController controller;
+    public final VibrationEffect.Composed effect;
+    public final int segmentIndex;
+    public final long previousStepVibratorOffTimeout;
+
+    long mVibratorOnResult;
+    boolean mVibratorCompleteCallbackReceived;
+
+    /**
+     * @param conductor          The VibrationStepConductor for these steps.
+     * @param startTime          The time to schedule this step in the
+     *                           {@link VibrationStepConductor}.
+     * @param controller         The vibrator that is playing the effect.
+     * @param effect             The effect being played in this step.
+     * @param index              The index of the next segment to be played by this step
+     * @param previousStepVibratorOffTimeout The time the vibrator is expected to complete any
+     *                           previous vibration and turn off. This is used to allow this step to
+     *                           be triggered when the completion callback is received, and can
+     *                           be used to play effects back-to-back.
+     */
+    AbstractVibratorStep(VibrationStepConductor conductor, long startTime,
+            VibratorController controller, VibrationEffect.Composed effect, int index,
+            long previousStepVibratorOffTimeout) {
+        super(conductor, startTime);
+        this.controller = controller;
+        this.effect = effect;
+        this.segmentIndex = index;
+        this.previousStepVibratorOffTimeout = previousStepVibratorOffTimeout;
+    }
+
+    public int getVibratorId() {
+        return controller.getVibratorInfo().getId();
+    }
+
+    @Override
+    public long getVibratorOnDuration() {
+        return mVibratorOnResult;
+    }
+
+    @Override
+    public boolean acceptVibratorCompleteCallback(int vibratorId) {
+        boolean isSameVibrator = controller.getVibratorInfo().getId() == vibratorId;
+        mVibratorCompleteCallbackReceived |= isSameVibrator;
+        // Only activate this step if a timeout was set to wait for the vibration to complete,
+        // otherwise we are waiting for the correct time to play the next step.
+        return isSameVibrator && (previousStepVibratorOffTimeout > SystemClock.uptimeMillis());
+    }
+
+    @Override
+    public List<Step> cancel() {
+        return Arrays.asList(new CompleteEffectVibratorStep(conductor, SystemClock.uptimeMillis(),
+                /* cancelled= */ true, controller, previousStepVibratorOffTimeout));
+    }
+
+    @Override
+    public void cancelImmediately() {
+        if (previousStepVibratorOffTimeout > SystemClock.uptimeMillis()) {
+            // Vibrator might be running from previous steps, so turn it off while canceling.
+            stopVibrating();
+        }
+    }
+
+    protected void stopVibrating() {
+        if (VibrationThread.DEBUG) {
+            Slog.d(VibrationThread.TAG,
+                    "Turning off vibrator " + getVibratorId());
+        }
+        controller.off();
+    }
+
+    protected void changeAmplitude(float amplitude) {
+        if (VibrationThread.DEBUG) {
+            Slog.d(VibrationThread.TAG,
+                    "Amplitude changed on vibrator " + getVibratorId() + " to " + amplitude);
+        }
+        controller.setAmplitude(amplitude);
+    }
+
+    /**
+     * Return the {@link VibrationStepConductor#nextVibrateStep} with same timings, only jumping
+     * the segments.
+     */
+    protected List<Step> skipToNextSteps(int segmentsSkipped) {
+        return nextSteps(startTime, previousStepVibratorOffTimeout, segmentsSkipped);
+    }
+
+    /**
+     * Return the {@link VibrationStepConductor#nextVibrateStep} with same start and off timings
+     * calculated from {@link #getVibratorOnDuration()}, jumping all played segments.
+     *
+     * <p>This method has same behavior as {@link #skipToNextSteps(int)} when the vibrator
+     * result is non-positive, meaning the vibrator has either ignored or failed to turn on.
+     */
+    protected List<Step> nextSteps(int segmentsPlayed) {
+        if (mVibratorOnResult <= 0) {
+            // Vibration was not started, so just skip the played segments and keep timings.
+            return skipToNextSteps(segmentsPlayed);
+        }
+        long nextStartTime = SystemClock.uptimeMillis() + mVibratorOnResult;
+        long nextVibratorOffTimeout =
+                nextStartTime + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
+        return nextSteps(nextStartTime, nextVibratorOffTimeout, segmentsPlayed);
+    }
+
+    /**
+     * Return the {@link VibrationStepConductor#nextVibrateStep} with given start and off timings,
+     * which might be calculated independently, jumping all played segments.
+     *
+     * <p>This should be used when the vibrator on/off state is not responsible for the steps
+     * execution timings, e.g. while playing the vibrator amplitudes.
+     */
+    protected List<Step> nextSteps(long nextStartTime, long vibratorOffTimeout,
+            int segmentsPlayed) {
+        Step nextStep = conductor.nextVibrateStep(nextStartTime, controller, effect,
+                segmentIndex + segmentsPlayed, vibratorOffTimeout);
+        return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST : Arrays.asList(nextStep);
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
new file mode 100644
index 0000000..8585e34
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/CompleteEffectVibratorStep.java
@@ -0,0 +1,114 @@
+/*
+ * 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.vibrator;
+
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents a step to complete a {@link VibrationEffect}.
+ *
+ * <p>This runs right at the time the vibration is considered to end and will update the pending
+ * vibrators count. This can turn off the vibrator or slowly ramp it down to zero amplitude.
+ */
+final class CompleteEffectVibratorStep extends AbstractVibratorStep {
+    private final boolean mCancelled;
+
+    CompleteEffectVibratorStep(VibrationStepConductor conductor, long startTime, boolean cancelled,
+            VibratorController controller, long previousStepVibratorOffTimeout) {
+        super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1,
+                previousStepVibratorOffTimeout);
+        mCancelled = cancelled;
+    }
+
+    @Override
+    public boolean isCleanUp() {
+        // If the vibration was cancelled then this is just a clean up to ramp off the vibrator.
+        // Otherwise this step is part of the vibration.
+        return mCancelled;
+    }
+
+    @Override
+    public List<Step> cancel() {
+        if (mCancelled) {
+            // Double cancelling will just turn off the vibrator right away.
+            return Arrays.asList(
+                    new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller));
+        }
+        return super.cancel();
+    }
+
+    @Override
+    public List<Step> play() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "CompleteEffectVibratorStep");
+        try {
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG,
+                        "Running " + (mCancelled ? "cancel" : "complete") + " vibration"
+                                + " step on vibrator " + controller.getVibratorInfo().getId());
+            }
+            if (mVibratorCompleteCallbackReceived) {
+                // Vibration completion callback was received by this step, just turn if off
+                // and skip any clean-up.
+                stopVibrating();
+                return VibrationStepConductor.EMPTY_STEP_LIST;
+            }
+
+            float currentAmplitude = controller.getCurrentAmplitude();
+            long remainingOnDuration =
+                    previousStepVibratorOffTimeout - VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT
+                            - SystemClock.uptimeMillis();
+            long rampDownDuration =
+                    Math.min(remainingOnDuration,
+                            conductor.vibrationSettings.getRampDownDuration());
+            long stepDownDuration = conductor.vibrationSettings.getRampStepDuration();
+            if (currentAmplitude < VibrationStepConductor.RAMP_OFF_AMPLITUDE_MIN
+                    || rampDownDuration <= stepDownDuration) {
+                // No need to ramp down the amplitude, just wait to turn it off.
+                if (mCancelled) {
+                    // Vibration is completing because it was cancelled, turn off right away.
+                    stopVibrating();
+                    return VibrationStepConductor.EMPTY_STEP_LIST;
+                } else {
+                    return Arrays.asList(new TurnOffVibratorStep(
+                            conductor, previousStepVibratorOffTimeout, controller));
+                }
+            }
+
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG,
+                        "Ramping down vibrator " + controller.getVibratorInfo().getId()
+                                + " from amplitude " + currentAmplitude
+                                + " for " + rampDownDuration + "ms");
+            }
+            float amplitudeDelta = currentAmplitude / (rampDownDuration / stepDownDuration);
+            float amplitudeTarget = currentAmplitude - amplitudeDelta;
+            long newVibratorOffTimeout =
+                    mCancelled ? rampDownDuration : previousStepVibratorOffTimeout;
+            return Arrays.asList(
+                    new RampOffVibratorStep(conductor, startTime, amplitudeTarget, amplitudeDelta,
+                            controller, newVibratorOffTimeout));
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
new file mode 100644
index 0000000..d1ea805
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a step to turn the vibrator on using a composition of primitives.
+ *
+ * <p>This step will use the maximum supported number of consecutive segments of type
+ * {@link PrimitiveSegment} starting at the current index.
+ */
+final class ComposePrimitivesVibratorStep extends AbstractVibratorStep {
+
+    ComposePrimitivesVibratorStep(VibrationStepConductor conductor, long startTime,
+            VibratorController controller, VibrationEffect.Composed effect, int index,
+            long previousStepVibratorOffTimeout) {
+        // This step should wait for the last vibration to finish (with the timeout) and for the
+        // intended step start time (to respect the effect delays).
+        super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
+                index, previousStepVibratorOffTimeout);
+    }
+
+    @Override
+    public List<Step> play() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep");
+        try {
+            // Load the next PrimitiveSegments to create a single compose call to the vibrator,
+            // limited to the vibrator composition maximum size.
+            int limit = controller.getVibratorInfo().getCompositionSizeMax();
+            int segmentCount = limit > 0
+                    ? Math.min(effect.getSegments().size(), segmentIndex + limit)
+                    : effect.getSegments().size();
+            List<PrimitiveSegment> primitives = new ArrayList<>();
+            for (int i = segmentIndex; i < segmentCount; i++) {
+                VibrationEffectSegment segment = effect.getSegments().get(i);
+                if (segment instanceof PrimitiveSegment) {
+                    primitives.add((PrimitiveSegment) segment);
+                } else {
+                    break;
+                }
+            }
+
+            if (primitives.isEmpty()) {
+                Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePrimitivesStep: "
+                        + effect.getSegments().get(segmentIndex));
+                return skipToNextSteps(/* segmentsSkipped= */ 1);
+            }
+
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG, "Compose " + primitives + " primitives on vibrator "
+                        + controller.getVibratorInfo().getId());
+            }
+            mVibratorOnResult = controller.on(
+                    primitives.toArray(new PrimitiveSegment[primitives.size()]),
+                    getVibration().id);
+
+            return nextSteps(/* segmentsPlayed= */ primitives.size());
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
new file mode 100644
index 0000000..73bf933
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.vibrator.RampSegment;
+import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a step to turn the vibrator on using a composition of PWLE segments.
+ *
+ * <p>This step will use the maximum supported number of consecutive segments of type
+ * {@link StepSegment} or {@link RampSegment} starting at the current index.
+ */
+final class ComposePwleVibratorStep extends AbstractVibratorStep {
+
+    ComposePwleVibratorStep(VibrationStepConductor conductor, long startTime,
+            VibratorController controller, VibrationEffect.Composed effect, int index,
+            long previousStepVibratorOffTimeout) {
+        // This step should wait for the last vibration to finish (with the timeout) and for the
+        // intended step start time (to respect the effect delays).
+        super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
+                index, previousStepVibratorOffTimeout);
+    }
+
+    @Override
+    public List<Step> play() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep");
+        try {
+            // Load the next RampSegments to create a single composePwle call to the vibrator,
+            // limited to the vibrator PWLE maximum size.
+            int limit = controller.getVibratorInfo().getPwleSizeMax();
+            int segmentCount = limit > 0
+                    ? Math.min(effect.getSegments().size(), segmentIndex + limit)
+                    : effect.getSegments().size();
+            List<RampSegment> pwles = new ArrayList<>();
+            for (int i = segmentIndex; i < segmentCount; i++) {
+                VibrationEffectSegment segment = effect.getSegments().get(i);
+                if (segment instanceof RampSegment) {
+                    pwles.add((RampSegment) segment);
+                } else {
+                    break;
+                }
+            }
+
+            if (pwles.isEmpty()) {
+                Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposePwleStep: "
+                        + effect.getSegments().get(segmentIndex));
+                return skipToNextSteps(/* segmentsSkipped= */ 1);
+            }
+
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG, "Compose " + pwles + " PWLEs on vibrator "
+                        + controller.getVibratorInfo().getId());
+            }
+            mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]),
+                    getVibration().id);
+
+            return nextSteps(/* segmentsPlayed= */ pwles.size());
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java
new file mode 100644
index 0000000..bbbca02
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/FinishSequentialEffectStep.java
@@ -0,0 +1,73 @@
+/*
+ * 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.vibrator;
+
+import android.os.Trace;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Finish a sync vibration started by a {@link StartSequentialEffectStep}.
+ *
+ * <p>This only plays after all active vibrators steps have finished, and adds a {@link
+ * StartSequentialEffectStep} to the queue if the sequential effect isn't finished yet.
+ */
+final class FinishSequentialEffectStep extends Step {
+    public final StartSequentialEffectStep startedStep;
+
+    FinishSequentialEffectStep(StartSequentialEffectStep startedStep) {
+        // No predefined startTime, just wait for all steps in the queue.
+        super(startedStep.conductor, Long.MAX_VALUE);
+        this.startedStep = startedStep;
+    }
+
+    @Override
+    public boolean isCleanUp() {
+        // This step only notes that all the vibrators has been turned off.
+        return true;
+    }
+
+    @Override
+    public List<Step> play() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "FinishSequentialEffectStep");
+        try {
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG,
+                        "FinishSequentialEffectStep for effect #" + startedStep.currentIndex);
+            }
+            conductor.vibratorManagerHooks.noteVibratorOff(conductor.getVibration().uid);
+            Step nextStep = startedStep.nextStep();
+            return nextStep == null ? VibrationStepConductor.EMPTY_STEP_LIST
+                    : Arrays.asList(nextStep);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+
+    @Override
+    public List<Step> cancel() {
+        cancelImmediately();
+        return VibrationStepConductor.EMPTY_STEP_LIST;
+    }
+
+    @Override
+    public void cancelImmediately() {
+        conductor.vibratorManagerHooks.noteVibratorOff(conductor.getVibration().uid);
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
new file mode 100644
index 0000000..601ae97
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java
@@ -0,0 +1,103 @@
+/*
+ * 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.vibrator;
+
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a step to turn the vibrator on with a single prebaked effect.
+ *
+ * <p>This step automatically falls back by replacing the prebaked segment with
+ * {@link VibrationSettings#getFallbackEffect(int)}, if available.
+ */
+final class PerformPrebakedVibratorStep extends AbstractVibratorStep {
+
+    PerformPrebakedVibratorStep(VibrationStepConductor conductor, long startTime,
+            VibratorController controller, VibrationEffect.Composed effect, int index,
+            long previousStepVibratorOffTimeout) {
+        // This step should wait for the last vibration to finish (with the timeout) and for the
+        // intended step start time (to respect the effect delays).
+        super(conductor, Math.max(startTime, previousStepVibratorOffTimeout), controller, effect,
+                index, previousStepVibratorOffTimeout);
+    }
+
+    @Override
+    public List<Step> play() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformPrebakedVibratorStep");
+        try {
+            VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
+            if (!(segment instanceof PrebakedSegment)) {
+                Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a "
+                        + "PerformPrebakedVibratorStep: " + segment);
+                return skipToNextSteps(/* segmentsSkipped= */ 1);
+            }
+
+            PrebakedSegment prebaked = (PrebakedSegment) segment;
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG, "Perform " + VibrationEffect.effectIdToString(
+                        prebaked.getEffectId()) + " on vibrator "
+                        + controller.getVibratorInfo().getId());
+            }
+
+            VibrationEffect fallback = getVibration().getFallback(prebaked.getEffectId());
+            mVibratorOnResult = controller.on(prebaked, getVibration().id);
+
+            if (mVibratorOnResult == 0 && prebaked.shouldFallback()
+                    && (fallback instanceof VibrationEffect.Composed)) {
+                if (VibrationThread.DEBUG) {
+                    Slog.d(VibrationThread.TAG, "Playing fallback for effect "
+                            + VibrationEffect.effectIdToString(prebaked.getEffectId()));
+                }
+                AbstractVibratorStep fallbackStep = conductor.nextVibrateStep(startTime, controller,
+                        replaceCurrentSegment((VibrationEffect.Composed) fallback),
+                        segmentIndex, previousStepVibratorOffTimeout);
+                List<Step> fallbackResult = fallbackStep.play();
+                // Update the result with the fallback result so this step is seamlessly
+                // replaced by the fallback to any outer application of this.
+                mVibratorOnResult = fallbackStep.getVibratorOnDuration();
+                return fallbackResult;
+            }
+
+            return nextSteps(/* segmentsPlayed= */ 1);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+
+    /**
+     * Replace segment at {@link #segmentIndex} in {@link #effect} with given fallback segments.
+     *
+     * @return a copy of {@link #effect} with replaced segment.
+     */
+    private VibrationEffect.Composed replaceCurrentSegment(VibrationEffect.Composed fallback) {
+        List<VibrationEffectSegment> newSegments = new ArrayList<>(effect.getSegments());
+        int newRepeatIndex = effect.getRepeatIndex();
+        newSegments.remove(segmentIndex);
+        newSegments.addAll(segmentIndex, fallback.getSegments());
+        if (segmentIndex < effect.getRepeatIndex()) {
+            newRepeatIndex += fallback.getSegments().size() - 1;
+        }
+        return new VibrationEffect.Composed(newSegments, newRepeatIndex);
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
new file mode 100644
index 0000000..8cf5fb3
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/RampOffVibratorStep.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Represents a step to ramp down the vibrator amplitude before turning it off. */
+final class RampOffVibratorStep extends AbstractVibratorStep {
+    private final float mAmplitudeTarget;
+    private final float mAmplitudeDelta;
+
+    RampOffVibratorStep(VibrationStepConductor conductor, long startTime, float amplitudeTarget,
+            float amplitudeDelta, VibratorController controller,
+            long previousStepVibratorOffTimeout) {
+        super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1,
+                previousStepVibratorOffTimeout);
+        mAmplitudeTarget = amplitudeTarget;
+        mAmplitudeDelta = amplitudeDelta;
+    }
+
+    @Override
+    public boolean isCleanUp() {
+        return true;
+    }
+
+    @Override
+    public List<Step> cancel() {
+        return Arrays.asList(
+                new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller));
+    }
+
+    @Override
+    public List<Step> play() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "RampOffVibratorStep");
+        try {
+            if (VibrationThread.DEBUG) {
+                long latency = SystemClock.uptimeMillis() - startTime;
+                Slog.d(VibrationThread.TAG, "Ramp down the vibrator amplitude, step with "
+                        + latency + "ms latency.");
+            }
+            if (mVibratorCompleteCallbackReceived) {
+                // Vibration completion callback was received by this step, just turn if off
+                // and skip the rest of the steps to ramp down the vibrator amplitude.
+                stopVibrating();
+                return VibrationStepConductor.EMPTY_STEP_LIST;
+            }
+
+            changeAmplitude(mAmplitudeTarget);
+
+            float newAmplitudeTarget = mAmplitudeTarget - mAmplitudeDelta;
+            if (newAmplitudeTarget < VibrationStepConductor.RAMP_OFF_AMPLITUDE_MIN) {
+                // Vibrator amplitude cannot go further down, just turn it off.
+                return Arrays.asList(new TurnOffVibratorStep(
+                        conductor, previousStepVibratorOffTimeout, controller));
+            }
+            return Arrays.asList(new RampOffVibratorStep(
+                    conductor,
+                    startTime + conductor.vibrationSettings.getRampStepDuration(),
+                    newAmplitudeTarget, mAmplitudeDelta, controller,
+                    previousStepVibratorOffTimeout));
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
new file mode 100644
index 0000000..d5c1116
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
@@ -0,0 +1,186 @@
+/*
+ * 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.vibrator;
+
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents a step to turn the vibrator on and change its amplitude.
+ *
+ * <p>This step ignores vibration completion callbacks and control the vibrator on/off state
+ * and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}.
+ */
+final class SetAmplitudeVibratorStep extends AbstractVibratorStep {
+    private long mNextOffTime;
+
+    SetAmplitudeVibratorStep(VibrationStepConductor conductor, long startTime,
+            VibratorController controller, VibrationEffect.Composed effect, int index,
+            long previousStepVibratorOffTimeout) {
+        // This step has a fixed startTime coming from the timings of the waveform it's playing.
+        super(conductor, startTime, controller, effect, index, previousStepVibratorOffTimeout);
+        mNextOffTime = previousStepVibratorOffTimeout;
+    }
+
+    @Override
+    public boolean acceptVibratorCompleteCallback(int vibratorId) {
+        if (controller.getVibratorInfo().getId() == vibratorId) {
+            mVibratorCompleteCallbackReceived = true;
+            mNextOffTime = SystemClock.uptimeMillis();
+        }
+        // Timings are tightly controlled here, so only trigger this step if the vibrator was
+        // supposed to be ON but has completed prematurely, to turn it back on as soon as
+        // possible.
+        return mNextOffTime < startTime && controller.getCurrentAmplitude() > 0;
+    }
+
+    @Override
+    public List<Step> play() {
+        // TODO: consider separating the "on" steps at the start into a separate Step.
+        // TODO: consider instantiating the step with the required amplitude, rather than
+        // needing to dig into the effect.
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "SetAmplitudeVibratorStep");
+        try {
+            long now = SystemClock.uptimeMillis();
+            long latency = now - startTime;
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG,
+                        "Running amplitude step with " + latency + "ms latency.");
+            }
+
+            if (mVibratorCompleteCallbackReceived && latency < 0) {
+                // This step was run early because the vibrator turned off prematurely.
+                // Turn it back on and return this same step to run at the exact right time.
+                mNextOffTime = turnVibratorBackOn(/* remainingDuration= */ -latency);
+                return Arrays.asList(new SetAmplitudeVibratorStep(conductor, startTime, controller,
+                        effect, segmentIndex, mNextOffTime));
+            }
+
+            VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
+            if (!(segment instanceof StepSegment)) {
+                Slog.w(VibrationThread.TAG,
+                        "Ignoring wrong segment for a SetAmplitudeVibratorStep: " + segment);
+                return skipToNextSteps(/* segmentsSkipped= */ 1);
+            }
+
+            StepSegment stepSegment = (StepSegment) segment;
+            if (stepSegment.getDuration() == 0) {
+                // Skip waveform entries with zero timing.
+                return skipToNextSteps(/* segmentsSkipped= */ 1);
+            }
+
+            float amplitude = stepSegment.getAmplitude();
+            if (amplitude == 0) {
+                if (previousStepVibratorOffTimeout > now) {
+                    // Amplitude cannot be set to zero, so stop the vibrator.
+                    stopVibrating();
+                    mNextOffTime = now;
+                }
+            } else {
+                if (startTime >= mNextOffTime) {
+                    // Vibrator is OFF. Turn vibrator back on for the duration of another
+                    // cycle before setting the amplitude.
+                    long onDuration = getVibratorOnDuration(effect, segmentIndex);
+                    if (onDuration > 0) {
+                        mVibratorOnResult = startVibrating(onDuration);
+                        mNextOffTime = now + onDuration
+                                + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
+                    }
+                }
+                changeAmplitude(amplitude);
+            }
+
+            // Use original startTime to avoid propagating latencies to the waveform.
+            long nextStartTime = startTime + segment.getDuration();
+            return nextSteps(nextStartTime, mNextOffTime, /* segmentsPlayed= */ 1);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+
+    private long turnVibratorBackOn(long remainingDuration) {
+        long onDuration = getVibratorOnDuration(effect, segmentIndex);
+        if (onDuration <= 0) {
+            // Vibrator is supposed to go back off when this step starts, so just leave it off.
+            return previousStepVibratorOffTimeout;
+        }
+        onDuration += remainingDuration;
+        float expectedAmplitude = controller.getCurrentAmplitude();
+        mVibratorOnResult = startVibrating(onDuration);
+        if (mVibratorOnResult > 0) {
+            // Set the amplitude back to the value it was supposed to be playing at.
+            changeAmplitude(expectedAmplitude);
+        }
+        return SystemClock.uptimeMillis() + onDuration
+                + VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
+    }
+
+    private long startVibrating(long duration) {
+        if (VibrationThread.DEBUG) {
+            Slog.d(VibrationThread.TAG,
+                    "Turning on vibrator " + controller.getVibratorInfo().getId() + " for "
+                            + duration + "ms");
+        }
+        return controller.on(duration, getVibration().id);
+    }
+
+    /**
+     * Get the duration the vibrator will be on for a waveform, starting at {@code startIndex}
+     * until the next time it's vibrating amplitude is zero or a different type of segment is
+     * found.
+     */
+    private long getVibratorOnDuration(VibrationEffect.Composed effect, int startIndex) {
+        List<VibrationEffectSegment> segments = effect.getSegments();
+        int segmentCount = segments.size();
+        int repeatIndex = effect.getRepeatIndex();
+        int i = startIndex;
+        long timing = 0;
+        while (i < segmentCount) {
+            VibrationEffectSegment segment = segments.get(i);
+            if (!(segment instanceof StepSegment)
+                    || ((StepSegment) segment).getAmplitude() == 0) {
+                break;
+            }
+            timing += segment.getDuration();
+            i++;
+            if (i == segmentCount && repeatIndex >= 0) {
+                i = repeatIndex;
+                // prevent infinite loop
+                repeatIndex = -1;
+            }
+            if (i == startIndex) {
+                // The repeating waveform keeps the vibrator ON all the time. Use a minimum
+                // of 1s duration to prevent short patterns from turning the vibrator ON too
+                // frequently.
+                return Math.max(timing, 1000);
+            }
+        }
+        if (i == segmentCount && effect.getRepeatIndex() < 0) {
+            // Vibration ending at non-zero amplitude, add extra timings to ramp down after
+            // vibration is complete.
+            timing += conductor.vibrationSettings.getRampDownDuration();
+        }
+        return timing;
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java
new file mode 100644
index 0000000..b8885e8
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/StartSequentialEffectStep.java
@@ -0,0 +1,369 @@
+/*
+ * 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.vibrator;
+
+import android.annotation.Nullable;
+import android.hardware.vibrator.IVibratorManager;
+import android.os.CombinedVibration;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.VibratorInfo;
+import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Starts a sync vibration.
+ *
+ * <p>If this step has successfully started playing a vibration on any vibrator, it will always
+ * add a {@link FinishSequentialEffectStep} to the queue, to be played after all vibrators
+ * have finished all their individual steps.
+ *
+ * <p>If this step does not start any vibrator, it will add a {@link StartSequentialEffectStep} if
+ * the sequential effect isn't finished yet.
+ *
+ * <p>TODO: this step actually does several things: multiple HAL calls to sync the vibrators,
+ * as well as dispatching the underlying vibrator instruction calls (which need to be done before
+ * triggering the synced effects). This role/encapsulation could probably be improved to split up
+ * the grouped HAL calls here, as well as to clarify the role of dispatching VibratorSteps between
+ * this class and the controller.
+ */
+final class StartSequentialEffectStep extends Step {
+    public final CombinedVibration.Sequential sequentialEffect;
+    public final int currentIndex;
+
+    private long mVibratorsOnMaxDuration;
+
+    StartSequentialEffectStep(VibrationStepConductor conductor,
+            CombinedVibration.Sequential effect) {
+        this(conductor, SystemClock.uptimeMillis() + effect.getDelays().get(0), effect,
+                /* index= */ 0);
+    }
+
+    StartSequentialEffectStep(VibrationStepConductor conductor, long startTime,
+            CombinedVibration.Sequential effect, int index) {
+        super(conductor, startTime);
+        sequentialEffect = effect;
+        currentIndex = index;
+    }
+
+    @Override
+    public long getVibratorOnDuration() {
+        return mVibratorsOnMaxDuration;
+    }
+
+    @Override
+    public List<Step> play() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "StartSequentialEffectStep");
+        List<Step> nextSteps = new ArrayList<>();
+        mVibratorsOnMaxDuration = -1;
+        try {
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG,
+                        "StartSequentialEffectStep for effect #" + currentIndex);
+            }
+            CombinedVibration effect = sequentialEffect.getEffects().get(currentIndex);
+            DeviceEffectMap effectMapping = createEffectToVibratorMapping(effect);
+            if (effectMapping == null) {
+                // Unable to map effects to vibrators, ignore this step.
+                return nextSteps;
+            }
+
+            mVibratorsOnMaxDuration = startVibrating(effectMapping, nextSteps);
+            if (mVibratorsOnMaxDuration > 0) {
+                conductor.vibratorManagerHooks.noteVibratorOn(conductor.getVibration().uid,
+                        mVibratorsOnMaxDuration);
+            }
+        } finally {
+            if (mVibratorsOnMaxDuration >= 0) {
+                // It least one vibrator was started then add a finish step to wait for all
+                // active vibrators to finish their individual steps before going to the next.
+                // Otherwise this step was ignored so just go to the next one.
+                Step nextStep =
+                        mVibratorsOnMaxDuration > 0 ? new FinishSequentialEffectStep(this)
+                                : nextStep();
+                if (nextStep != null) {
+                    nextSteps.add(nextStep);
+                }
+            }
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+        return nextSteps;
+    }
+
+    @Override
+    public List<Step> cancel() {
+        return VibrationStepConductor.EMPTY_STEP_LIST;
+    }
+
+    @Override
+    public void cancelImmediately() {
+    }
+
+    /**
+     * Create the next {@link StartSequentialEffectStep} to play this sequential effect, starting at
+     * the
+     * time this method is called, or null if sequence is complete.
+     */
+    @Nullable
+    Step nextStep() {
+        int nextIndex = currentIndex + 1;
+        if (nextIndex >= sequentialEffect.getEffects().size()) {
+            return null;
+        }
+        long nextEffectDelay = sequentialEffect.getDelays().get(nextIndex);
+        long nextStartTime = SystemClock.uptimeMillis() + nextEffectDelay;
+        return new StartSequentialEffectStep(conductor, nextStartTime, sequentialEffect,
+                nextIndex);
+    }
+
+    /** Create a mapping of individual {@link VibrationEffect} to available vibrators. */
+    @Nullable
+    private DeviceEffectMap createEffectToVibratorMapping(
+            CombinedVibration effect) {
+        if (effect instanceof CombinedVibration.Mono) {
+            return new DeviceEffectMap((CombinedVibration.Mono) effect);
+        }
+        if (effect instanceof CombinedVibration.Stereo) {
+            return new DeviceEffectMap((CombinedVibration.Stereo) effect);
+        }
+        return null;
+    }
+
+    /**
+     * Starts playing effects on designated vibrators, in sync.
+     *
+     * @param effectMapping The {@link CombinedVibration} mapped to this device vibrators
+     * @param nextSteps     An output list to accumulate the future {@link Step
+     *                      Steps} created
+     *                      by this method, typically one for each vibrator that has
+     *                      successfully started vibrating on this step.
+     * @return The duration, in millis, of the {@link CombinedVibration}. Repeating
+     * waveforms return {@link Long#MAX_VALUE}. Zero or negative values indicate the vibrators
+     * have ignored all effects.
+     */
+    private long startVibrating(
+            DeviceEffectMap effectMapping, List<Step> nextSteps) {
+        int vibratorCount = effectMapping.size();
+        if (vibratorCount == 0) {
+            // No effect was mapped to any available vibrator.
+            return 0;
+        }
+
+        AbstractVibratorStep[] steps = new AbstractVibratorStep[vibratorCount];
+        long vibrationStartTime = SystemClock.uptimeMillis();
+        for (int i = 0; i < vibratorCount; i++) {
+            steps[i] = conductor.nextVibrateStep(vibrationStartTime,
+                    conductor.getVibrators().get(effectMapping.vibratorIdAt(i)),
+                    effectMapping.effectAt(i),
+                    /* segmentIndex= */ 0, /* vibratorOffTimeout= */ 0);
+        }
+
+        if (steps.length == 1) {
+            // No need to prepare and trigger sync effects on a single vibrator.
+            return startVibrating(steps[0], nextSteps);
+        }
+
+        // This synchronization of vibrators should be executed one at a time, even if we are
+        // vibrating different sets of vibrators in parallel. The manager can only prepareSynced
+        // one set of vibrators at a time.
+        // This property is guaranteed by there only being one thread (VibrationThread) executing
+        // one Step at a time, so there's no need to hold the state lock. Callbacks will be
+        // delivered asynchronously but enqueued until the step processing is finished.
+        boolean hasPrepared = false;
+        boolean hasTriggered = false;
+        long maxDuration = 0;
+        try {
+            hasPrepared = conductor.vibratorManagerHooks.prepareSyncedVibration(
+                    effectMapping.getRequiredSyncCapabilities(),
+                    effectMapping.getVibratorIds());
+
+            for (AbstractVibratorStep step : steps) {
+                long duration = startVibrating(step, nextSteps);
+                if (duration < 0) {
+                    // One vibrator has failed, fail this entire sync attempt.
+                    return maxDuration = -1;
+                }
+                maxDuration = Math.max(maxDuration, duration);
+            }
+
+            // Check if sync was prepared and if any step was accepted by a vibrator,
+            // otherwise there is nothing to trigger here.
+            if (hasPrepared && maxDuration > 0) {
+                hasTriggered = conductor.vibratorManagerHooks.triggerSyncedVibration(
+                        getVibration().id);
+            }
+            return maxDuration;
+        } finally {
+            if (hasPrepared && !hasTriggered) {
+                // Trigger has failed or all steps were ignored by the vibrators.
+                conductor.vibratorManagerHooks.cancelSyncedVibration();
+                nextSteps.clear();
+            } else if (maxDuration < 0) {
+                // Some vibrator failed without being prepared so other vibrators might be
+                // active. Cancel and remove every pending step from output list.
+                for (int i = nextSteps.size() - 1; i >= 0; i--) {
+                    nextSteps.remove(i).cancelImmediately();
+                }
+            }
+        }
+    }
+
+    private long startVibrating(AbstractVibratorStep step, List<Step> nextSteps) {
+        nextSteps.addAll(step.play());
+        long stepDuration = step.getVibratorOnDuration();
+        if (stepDuration < 0) {
+            // Step failed, so return negative duration to propagate failure.
+            return stepDuration;
+        }
+        // Return the longest estimation for the entire effect.
+        return Math.max(stepDuration, step.effect.getDuration());
+    }
+
+    /**
+     * Map a {@link CombinedVibration} to the vibrators available on the device.
+     *
+     * <p>This contains the logic to find the capabilities required from {@link IVibratorManager} to
+     * play all of the effects in sync.
+     */
+    final class DeviceEffectMap {
+        private final SparseArray<VibrationEffect.Composed> mVibratorEffects;
+        private final int[] mVibratorIds;
+        private final long mRequiredSyncCapabilities;
+
+        DeviceEffectMap(CombinedVibration.Mono mono) {
+            SparseArray<VibratorController> vibrators = conductor.getVibrators();
+            mVibratorEffects = new SparseArray<>(vibrators.size());
+            mVibratorIds = new int[vibrators.size()];
+            for (int i = 0; i < vibrators.size(); i++) {
+                int vibratorId = vibrators.keyAt(i);
+                VibratorInfo vibratorInfo = vibrators.valueAt(i).getVibratorInfo();
+                VibrationEffect effect = conductor.deviceEffectAdapter.apply(
+                        mono.getEffect(), vibratorInfo);
+                if (effect instanceof VibrationEffect.Composed) {
+                    mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect);
+                    mVibratorIds[i] = vibratorId;
+                }
+            }
+            mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects);
+        }
+
+        DeviceEffectMap(CombinedVibration.Stereo stereo) {
+            SparseArray<VibratorController> vibrators = conductor.getVibrators();
+            SparseArray<VibrationEffect> stereoEffects = stereo.getEffects();
+            mVibratorEffects = new SparseArray<>();
+            for (int i = 0; i < stereoEffects.size(); i++) {
+                int vibratorId = stereoEffects.keyAt(i);
+                if (vibrators.contains(vibratorId)) {
+                    VibratorInfo vibratorInfo = vibrators.valueAt(i).getVibratorInfo();
+                    VibrationEffect effect = conductor.deviceEffectAdapter.apply(
+                            stereoEffects.valueAt(i), vibratorInfo);
+                    if (effect instanceof VibrationEffect.Composed) {
+                        mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect);
+                    }
+                }
+            }
+            mVibratorIds = new int[mVibratorEffects.size()];
+            for (int i = 0; i < mVibratorEffects.size(); i++) {
+                mVibratorIds[i] = mVibratorEffects.keyAt(i);
+            }
+            mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects);
+        }
+
+        /**
+         * Return the number of vibrators mapped to play the {@link CombinedVibration} on this
+         * device.
+         */
+        public int size() {
+            return mVibratorIds.length;
+        }
+
+        /**
+         * Return all capabilities required to play the {@link CombinedVibration} in
+         * between calls to {@link IVibratorManager#prepareSynced} and
+         * {@link IVibratorManager#triggerSynced}.
+         */
+        public long getRequiredSyncCapabilities() {
+            return mRequiredSyncCapabilities;
+        }
+
+        /** Return all vibrator ids mapped to play the {@link CombinedVibration}. */
+        public int[] getVibratorIds() {
+            return mVibratorIds;
+        }
+
+        /** Return the id of the vibrator at given index. */
+        public int vibratorIdAt(int index) {
+            return mVibratorEffects.keyAt(index);
+        }
+
+        /** Return the {@link VibrationEffect} at given index. */
+        public VibrationEffect.Composed effectAt(int index) {
+            return mVibratorEffects.valueAt(index);
+        }
+
+        /**
+         * Return all capabilities required from the {@link IVibratorManager} to prepare and
+         * trigger all given effects in sync.
+         *
+         * @return {@link IVibratorManager#CAP_SYNC} together with all required
+         * IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities.
+         */
+        private long calculateRequiredSyncCapabilities(
+                SparseArray<VibrationEffect.Composed> effects) {
+            long prepareCap = 0;
+            for (int i = 0; i < effects.size(); i++) {
+                VibrationEffectSegment firstSegment = effects.valueAt(i).getSegments().get(0);
+                if (firstSegment instanceof StepSegment) {
+                    prepareCap |= IVibratorManager.CAP_PREPARE_ON;
+                } else if (firstSegment instanceof PrebakedSegment) {
+                    prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM;
+                } else if (firstSegment instanceof PrimitiveSegment) {
+                    prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE;
+                }
+            }
+            int triggerCap = 0;
+            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_ON)) {
+                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_ON;
+            }
+            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_PERFORM)) {
+                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_PERFORM;
+            }
+            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_COMPOSE)) {
+                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE;
+            }
+            return IVibratorManager.CAP_SYNC | prepareCap | triggerCap;
+        }
+
+        /**
+         * Return true if {@code prepareCapabilities} contains this {@code capability} mixed with
+         * different ones, requiring a mixed trigger capability from the vibrator manager for
+         * syncing all effects.
+         */
+        private boolean requireMixedTriggerCapability(long prepareCapabilities, long capability) {
+            return (prepareCapabilities & capability) != 0
+                    && (prepareCapabilities & ~capability) != 0;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/Step.java b/services/core/java/com/android/server/vibrator/Step.java
new file mode 100644
index 0000000..042e8a0
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/Step.java
@@ -0,0 +1,103 @@
+/*
+ * 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.vibrator;
+
+import android.annotation.NonNull;
+import android.os.CombinedVibration;
+import android.os.SystemClock;
+import android.os.VibrationEffect;
+
+import java.util.List;
+
+/**
+ * Represent a single step for playing a vibration.
+ *
+ * <p>Every step has a start time, which can be used to apply delays between steps while
+ * executing them in sequence.
+ */
+abstract class Step implements Comparable<Step> {
+    public final VibrationStepConductor conductor;
+    public final long startTime;
+
+    Step(VibrationStepConductor conductor, long startTime) {
+        this.conductor = conductor;
+        this.startTime = startTime;
+    }
+
+    protected Vibration getVibration() {
+        return conductor.getVibration();
+    }
+
+    /**
+     * Returns true if this step is a clean up step and not part of a {@link VibrationEffect} or
+     * {@link CombinedVibration}.
+     */
+    public boolean isCleanUp() {
+        return false;
+    }
+
+    /** Play this step, returning a (possibly empty) list of next steps. */
+    @NonNull
+    public abstract List<Step> play();
+
+    /**
+     * Cancel this pending step and return a (possibly empty) list of clean-up steps that should
+     * be played to gracefully cancel this step.
+     */
+    @NonNull
+    public abstract List<Step> cancel();
+
+    /** Cancel this pending step immediately, skipping any clean-up. */
+    public abstract void cancelImmediately();
+
+    /**
+     * Return the duration the vibrator was turned on when this step was played.
+     *
+     * @return A positive duration that the vibrator was turned on for by this step;
+     * Zero if the segment is not supported, the step was not played yet or vibrator was never
+     * turned on by this step; A negative value if the vibrator call has failed.
+     */
+    public long getVibratorOnDuration() {
+        return 0;
+    }
+
+    /**
+     * Return true to run this step right after a vibrator has notified vibration completed,
+     * used to resume steps waiting on vibrator callbacks with a timeout.
+     */
+    public boolean acceptVibratorCompleteCallback(int vibratorId) {
+        return false;
+    }
+
+    /**
+     * Returns the time in millis to wait before playing this step. This is performed
+     * while holding the queue lock, so should not rely on potentially slow operations.
+     */
+    public long calculateWaitTime() {
+        if (startTime == Long.MAX_VALUE) {
+            // This step don't have a predefined start time, it's just marked to be executed
+            // after all other steps have finished.
+            return 0;
+        }
+        return Math.max(0, startTime - SystemClock.uptimeMillis());
+    }
+
+    @Override
+    public int compareTo(Step o) {
+        return Long.compare(startTime, o.startTime);
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java
new file mode 100644
index 0000000..297ef56
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/TurnOffVibratorStep.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.os.SystemClock;
+import android.os.Trace;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents a step to turn the vibrator off.
+ *
+ * <p>This runs after a timeout on the expected time the vibrator should have finished playing,
+ * and can be brought forward by vibrator complete callbacks. The step shouldn't be skipped, even
+ * if the vibrator-complete callback was received, as some implementations still rely on the
+ * "off" call to actually stop.
+ */
+final class TurnOffVibratorStep extends AbstractVibratorStep {
+
+    TurnOffVibratorStep(VibrationStepConductor conductor, long startTime,
+            VibratorController controller) {
+        super(conductor, startTime, controller, /* effect= */ null, /* index= */ -1, startTime);
+    }
+
+    @Override
+    public boolean isCleanUp() {
+        return true;
+    }
+
+    @Override
+    public List<Step> cancel() {
+        return Arrays.asList(
+                new TurnOffVibratorStep(conductor, SystemClock.uptimeMillis(), controller));
+    }
+
+    @Override
+    public void cancelImmediately() {
+        stopVibrating();
+    }
+
+    @Override
+    public List<Step> play() {
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "TurnOffVibratorStep");
+        try {
+            stopVibrating();
+            return VibrationStepConductor.EMPTY_STEP_LIST;
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
new file mode 100644
index 0000000..51691fb
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -0,0 +1,355 @@
+/*
+ * 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.vibrator;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.CombinedVibration;
+import android.os.VibrationEffect;
+import android.os.WorkSource;
+import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.RampSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.PriorityQueue;
+import java.util.Queue;
+
+/**
+ * Creates and manages a queue of steps for performing a VibrationEffect, as well as coordinating
+ * dispatch of callbacks.
+ */
+final class VibrationStepConductor {
+    /**
+     * Extra timeout added to the end of each vibration step to ensure it finishes even when
+     * vibrator callbacks are lost.
+     */
+    static final long CALLBACKS_EXTRA_TIMEOUT = 1_000;
+    /** Threshold to prevent the ramp off steps from trying to set extremely low amplitudes. */
+    static final float RAMP_OFF_AMPLITUDE_MIN = 1e-3f;
+    static final List<Step> EMPTY_STEP_LIST = new ArrayList<>();
+
+    final Object mLock = new Object();
+
+    // Used within steps.
+    public final VibrationSettings vibrationSettings;
+    public final DeviceVibrationEffectAdapter deviceEffectAdapter;
+    public final VibrationThread.VibratorManagerHooks vibratorManagerHooks;
+
+    private final WorkSource mWorkSource;
+    private final Vibration mVibration;
+    private final SparseArray<VibratorController> mVibrators = new SparseArray<>();
+
+    @GuardedBy("mLock")
+    private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>();
+    @GuardedBy("mLock")
+    private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>();
+    @GuardedBy("mLock")
+    private final Queue<Integer> mCompletionNotifiedVibrators = new LinkedList<>();
+
+    @GuardedBy("mLock")
+    private int mPendingVibrateSteps;
+    @GuardedBy("mLock")
+    private int mConsumedStartVibrateSteps;
+    @GuardedBy("mLock")
+    private int mSuccessfulVibratorOnSteps;
+    @GuardedBy("mLock")
+    private boolean mWaitToProcessVibratorCompleteCallbacks;
+
+    VibrationStepConductor(Vibration vib, VibrationSettings vibrationSettings,
+            DeviceVibrationEffectAdapter effectAdapter,
+            SparseArray<VibratorController> availableVibrators,
+            VibrationThread.VibratorManagerHooks vibratorManagerHooks) {
+        this.mVibration = vib;
+        this.vibrationSettings = vibrationSettings;
+        this.deviceEffectAdapter = effectAdapter;
+        this.vibratorManagerHooks = vibratorManagerHooks;
+        this.mWorkSource = new WorkSource(mVibration.uid);
+
+        CombinedVibration effect = vib.getEffect();
+        for (int i = 0; i < availableVibrators.size(); i++) {
+            if (effect.hasVibrator(availableVibrators.keyAt(i))) {
+                mVibrators.put(availableVibrators.keyAt(i), availableVibrators.valueAt(i));
+            }
+        }
+    }
+
+    @Nullable
+    AbstractVibratorStep nextVibrateStep(long startTime, VibratorController controller,
+            VibrationEffect.Composed effect, int segmentIndex,
+            long previousStepVibratorOffTimeout) {
+        if (segmentIndex >= effect.getSegments().size()) {
+            segmentIndex = effect.getRepeatIndex();
+        }
+        if (segmentIndex < 0) {
+            // No more segments to play, last step is to complete the vibration on this vibrator.
+            return new CompleteEffectVibratorStep(this, startTime, /* cancelled= */ false,
+                    controller, previousStepVibratorOffTimeout);
+        }
+
+        VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
+        if (segment instanceof PrebakedSegment) {
+            return new PerformPrebakedVibratorStep(this, startTime, controller, effect,
+                    segmentIndex, previousStepVibratorOffTimeout);
+        }
+        if (segment instanceof PrimitiveSegment) {
+            return new ComposePrimitivesVibratorStep(this, startTime, controller, effect,
+                    segmentIndex, previousStepVibratorOffTimeout);
+        }
+        if (segment instanceof RampSegment) {
+            return new ComposePwleVibratorStep(this, startTime, controller, effect, segmentIndex,
+                    previousStepVibratorOffTimeout);
+        }
+        return new SetAmplitudeVibratorStep(this, startTime, controller, effect, segmentIndex,
+                previousStepVibratorOffTimeout);
+    }
+
+    public void initializeForEffect(@NonNull CombinedVibration.Sequential vibration) {
+        synchronized (mLock) {
+            mPendingVibrateSteps++;
+            mNextSteps.offer(new StartSequentialEffectStep(this, vibration));
+        }
+    }
+
+    public Vibration getVibration() {
+        return mVibration;
+    }
+
+    public WorkSource getWorkSource() {
+        return mWorkSource;
+    }
+
+    SparseArray<VibratorController> getVibrators() {
+        return mVibrators;
+    }
+
+    public boolean isFinished() {
+        synchronized (mLock) {
+            return mPendingOnVibratorCompleteSteps.isEmpty() && mNextSteps.isEmpty();
+        }
+    }
+
+    /**
+     * Calculate the {@link Vibration.Status} based on the current queue state and the expected
+     * number of {@link StartSequentialEffectStep} to be played.
+     */
+    public Vibration.Status calculateVibrationStatus(int expectedStartVibrateSteps) {
+        synchronized (mLock) {
+            if (mPendingVibrateSteps > 0
+                    || mConsumedStartVibrateSteps < expectedStartVibrateSteps) {
+                return Vibration.Status.RUNNING;
+            }
+            if (mSuccessfulVibratorOnSteps > 0) {
+                return Vibration.Status.FINISHED;
+            }
+            // If no step was able to turn the vibrator ON successfully.
+            return Vibration.Status.IGNORED_UNSUPPORTED;
+        }
+    }
+
+    /** Returns the time in millis to wait before calling {@link #runNextStep()}. */
+    @GuardedBy("mLock")
+    public long getWaitMillisBeforeNextStepLocked() {
+        if (!mPendingOnVibratorCompleteSteps.isEmpty()) {
+            // Steps resumed by vibrator complete callback should be played right away.
+            return 0;
+        }
+        Step nextStep = mNextSteps.peek();
+        return nextStep == null ? 0 : nextStep.calculateWaitTime();
+    }
+
+    /**
+     * Play and remove the step at the top of this queue, and also adds the next steps generated
+     * to be played next.
+     */
+    public void runNextStep() {
+        // Vibrator callbacks should wait until the polled step is played and the next steps are
+        // added back to the queue, so they can handle the callback.
+        markWaitToProcessVibratorCallbacks();
+        try {
+            Step nextStep = pollNext();
+            if (nextStep != null) {
+                // This might turn on the vibrator and have a HAL latency. Execute this outside
+                // any lock to avoid blocking other interactions with the thread.
+                List<Step> nextSteps = nextStep.play();
+                synchronized (mLock) {
+                    if (nextStep.getVibratorOnDuration() > 0) {
+                        mSuccessfulVibratorOnSteps++;
+                    }
+                    if (nextStep instanceof StartSequentialEffectStep) {
+                        mConsumedStartVibrateSteps++;
+                    }
+                    if (!nextStep.isCleanUp()) {
+                        mPendingVibrateSteps--;
+                    }
+                    for (int i = 0; i < nextSteps.size(); i++) {
+                        mPendingVibrateSteps += nextSteps.get(i).isCleanUp() ? 0 : 1;
+                    }
+                    mNextSteps.addAll(nextSteps);
+                }
+            }
+        } finally {
+            synchronized (mLock) {
+                processVibratorCompleteCallbacksLocked();
+            }
+        }
+    }
+
+    /**
+     * Notify the vibrator completion.
+     *
+     * <p>This is a lightweight method that do not trigger any operation from {@link
+     * VibratorController}, so it can be called directly from a native callback.
+     */
+    @GuardedBy("mLock")
+    private void notifyVibratorCompleteLocked(int vibratorId) {
+        mCompletionNotifiedVibrators.offer(vibratorId);
+        if (!mWaitToProcessVibratorCompleteCallbacks) {
+            // No step is being played or cancelled now, process the callback right away.
+            processVibratorCompleteCallbacksLocked();
+        }
+    }
+
+    public void notifyVibratorComplete(int vibratorId) {
+        synchronized (mLock) {
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG,
+                        "Vibration complete reported by vibrator " + vibratorId);
+            }
+            notifyVibratorCompleteLocked(vibratorId);
+            mLock.notify();
+        }
+    }
+
+    public void notifySyncedVibrationComplete() {
+        synchronized (mLock) {
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG,
+                        "Synced vibration complete reported by vibrator manager");
+            }
+            for (int i = 0; i < mVibrators.size(); i++) {
+                notifyVibratorCompleteLocked(mVibrators.keyAt(i));
+            }
+            mLock.notify();
+        }
+    }
+
+    /**
+     * Cancel the current queue, replacing all remaining steps with respective clean-up steps.
+     *
+     * <p>This will remove all steps and replace them with respective
+     * {@link Step#cancel()}.
+     */
+    public void cancel() {
+        // Vibrator callbacks should wait until all steps from the queue are properly cancelled
+        // and clean up steps are added back to the queue, so they can handle the callback.
+        markWaitToProcessVibratorCallbacks();
+        try {
+            List<Step> cleanUpSteps = new ArrayList<>();
+            Step step;
+            while ((step = pollNext()) != null) {
+                cleanUpSteps.addAll(step.cancel());
+            }
+            synchronized (mLock) {
+                // All steps generated by Step.cancel() should be clean-up steps.
+                mPendingVibrateSteps = 0;
+                mNextSteps.addAll(cleanUpSteps);
+            }
+        } finally {
+            synchronized (mLock) {
+                processVibratorCompleteCallbacksLocked();
+            }
+        }
+    }
+
+    /**
+     * Cancel the current queue immediately, clearing all remaining steps and skipping clean-up.
+     *
+     * <p>This will remove and trigger {@link Step#cancelImmediately()} in all steps, in order.
+     */
+    public void cancelImmediately() {
+        // Vibrator callbacks should wait until all steps from the queue are properly cancelled.
+        markWaitToProcessVibratorCallbacks();
+        try {
+            Step step;
+            while ((step = pollNext()) != null) {
+                // This might turn off the vibrator and have a HAL latency. Execute this outside
+                // any lock to avoid blocking other interactions with the thread.
+                step.cancelImmediately();
+            }
+            synchronized (mLock) {
+                mPendingVibrateSteps = 0;
+            }
+        } finally {
+            synchronized (mLock) {
+                processVibratorCompleteCallbacksLocked();
+            }
+        }
+    }
+
+    @Nullable
+    private Step pollNext() {
+        synchronized (mLock) {
+            // Prioritize the steps resumed by a vibrator complete callback.
+            if (!mPendingOnVibratorCompleteSteps.isEmpty()) {
+                return mPendingOnVibratorCompleteSteps.poll();
+            }
+            return mNextSteps.poll();
+        }
+    }
+
+    private void markWaitToProcessVibratorCallbacks() {
+        synchronized (mLock) {
+            mWaitToProcessVibratorCompleteCallbacks = true;
+        }
+    }
+
+    /**
+     * Notify the step in this queue that should be resumed by the vibrator completion
+     * callback and keep it separate to be consumed by {@link #runNextStep()}.
+     *
+     * <p>This is a lightweight method that do not trigger any operation from {@link
+     * VibratorController}, so it can be called directly from a native callback.
+     *
+     * <p>This assumes only one of the next steps is waiting on this given vibrator, so the
+     * first step found will be resumed by this method, in no particular order.
+     */
+    @GuardedBy("mLock")
+    private void processVibratorCompleteCallbacksLocked() {
+        mWaitToProcessVibratorCompleteCallbacks = false;
+        while (!mCompletionNotifiedVibrators.isEmpty()) {
+            int vibratorId = mCompletionNotifiedVibrators.poll();
+            Iterator<Step> it = mNextSteps.iterator();
+            while (it.hasNext()) {
+                Step step = it.next();
+                if (step.acceptVibratorCompleteCallback(vibratorId)) {
+                    it.remove();
+                    mPendingOnVibratorCompleteSteps.offer(step);
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 1f1f40b..f2cd8c3 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -16,59 +16,23 @@
 
 package com.android.server.vibrator;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.hardware.vibrator.IVibratorManager;
 import android.os.CombinedVibration;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.SystemClock;
 import android.os.Trace;
-import android.os.VibrationEffect;
-import android.os.VibratorInfo;
-import android.os.WorkSource;
-import android.os.vibrator.PrebakedSegment;
-import android.os.vibrator.PrimitiveSegment;
-import android.os.vibrator.RampSegment;
-import android.os.vibrator.StepSegment;
-import android.os.vibrator.VibrationEffectSegment;
 import android.util.Slog;
 import android.util.SparseArray;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.app.IBatteryStats;
-import com.android.internal.util.FrameworkStatsLog;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.NoSuchElementException;
-import java.util.PriorityQueue;
-import java.util.Queue;
 
 /** Plays a {@link Vibration} in dedicated thread. */
 final class VibrationThread extends Thread implements IBinder.DeathRecipient {
-    private static final String TAG = "VibrationThread";
-    private static final boolean DEBUG = false;
-
-    /**
-     * Extra timeout added to the end of each vibration step to ensure it finishes even when
-     * vibrator callbacks are lost.
-     */
-    private static final long CALLBACKS_EXTRA_TIMEOUT = 1_000;
-
-    /** Threshold to prevent the ramp off steps from trying to set extremely low amplitudes. */
-    private static final float RAMP_OFF_AMPLITUDE_MIN = 1e-3f;
-
-    /** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */
-    private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000;
-
-    private static final List<Step> EMPTY_STEP_LIST = new ArrayList<>();
+    static final String TAG = "VibrationThread";
+    static final boolean DEBUG = false;
 
     /** Calls into VibratorManager functionality needed for playing a {@link Vibration}. */
     interface VibratorManagerHooks {
@@ -94,6 +58,15 @@
         void cancelSyncedVibration();
 
         /**
+         * Record that a vibrator was turned on, and may remain on for the specified duration,
+         * on behalf of the given uid.
+         */
+        void noteVibratorOn(int uid, long duration);
+
+        /** Record that a vibrator was turned off, on behalf of the given uid. */
+        void noteVibratorOff(int uid);
+
+        /**
          * Tell the manager that the currently active vibration has completed its vibration, from
          * the perspective of the Effect. However, the VibrationThread may still be continuing with
          * cleanup tasks, and should not be given new work until {@link #onVibrationThreadReleased}
@@ -108,16 +81,10 @@
         void onVibrationThreadReleased();
     }
 
-    private final Object mLock = new Object();
-    private final WorkSource mWorkSource;
     private final PowerManager.WakeLock mWakeLock;
-    private final IBatteryStats mBatteryStatsService;
-    private final VibrationSettings mVibrationSettings;
-    private final DeviceVibrationEffectAdapter mDeviceEffectAdapter;
-    private final Vibration mVibration;
-    private final VibratorManagerHooks mVibratorManagerHooks;
-    private final SparseArray<VibratorController> mVibrators = new SparseArray<>();
-    private final StepQueue mStepQueue = new StepQueue();
+    private final VibrationThread.VibratorManagerHooks mVibratorManagerHooks;
+
+    private final VibrationStepConductor mStepConductor;
 
     private volatile boolean mStop;
     private volatile boolean mForceStop;
@@ -127,30 +94,20 @@
     VibrationThread(Vibration vib, VibrationSettings vibrationSettings,
             DeviceVibrationEffectAdapter effectAdapter,
             SparseArray<VibratorController> availableVibrators, PowerManager.WakeLock wakeLock,
-            IBatteryStats batteryStatsService, VibratorManagerHooks vibratorManagerHooks) {
-        mVibration = vib;
-        mVibrationSettings = vibrationSettings;
-        mDeviceEffectAdapter = effectAdapter;
+            VibratorManagerHooks vibratorManagerHooks) {
         mVibratorManagerHooks = vibratorManagerHooks;
-        mWorkSource = new WorkSource(mVibration.uid);
         mWakeLock = wakeLock;
-        mBatteryStatsService = batteryStatsService;
-
-        CombinedVibration effect = vib.getEffect();
-        for (int i = 0; i < availableVibrators.size(); i++) {
-            if (effect.hasVibrator(availableVibrators.keyAt(i))) {
-                mVibrators.put(availableVibrators.keyAt(i), availableVibrators.valueAt(i));
-            }
-        }
+        mStepConductor = new VibrationStepConductor(vib, vibrationSettings, effectAdapter,
+                availableVibrators, vibratorManagerHooks);
     }
 
     Vibration getVibration() {
-        return mVibration;
+        return mStepConductor.getVibration();
     }
 
     @VisibleForTesting
     SparseArray<VibratorController> getVibrators() {
-        return mVibrators;
+        return mStepConductor.getVibrators();
     }
 
     @Override
@@ -179,7 +136,7 @@
 
     /** Runs the VibrationThread ensuring that the wake lock is acquired and released. */
     private void runWithWakeLock() {
-        mWakeLock.setWorkSource(mWorkSource);
+        mWakeLock.setWorkSource(mStepConductor.getWorkSource());
         mWakeLock.acquire();
         try {
             runWithWakeLockAndDeathLink();
@@ -193,8 +150,9 @@
      * Called from within runWithWakeLock.
      */
     private void runWithWakeLockAndDeathLink() {
+        IBinder vibrationBinderToken = mStepConductor.getVibration().token;
         try {
-            mVibration.token.linkToDeath(this, 0);
+            vibrationBinderToken.linkToDeath(this, 0);
         } catch (RemoteException e) {
             Slog.e(TAG, "Error linking vibration to token death", e);
             clientVibrationCompleteIfNotAlready(Vibration.Status.IGNORED_ERROR_TOKEN);
@@ -206,7 +164,7 @@
             playVibration();
         } finally {
             try {
-                mVibration.token.unlinkToDeath(this, 0);
+                vibrationBinderToken.unlinkToDeath(this, 0);
             } catch (NoSuchElementException e) {
                 Slog.wtf(TAG, "Failed to unlink token", e);
             }
@@ -220,11 +178,11 @@
             return;
         }
         mStop = true;
-        synchronized (mLock) {
+        synchronized (mStepConductor.mLock) {
             if (DEBUG) {
                 Slog.d(TAG, "Vibration cancelled");
             }
-            mLock.notify();
+            mStepConductor.mLock.notify();
         }
     }
 
@@ -235,36 +193,22 @@
             return;
         }
         mStop = mForceStop = true;
-        synchronized (mLock) {
+        synchronized (mStepConductor.mLock) {
             if (DEBUG) {
                 Slog.d(TAG, "Vibration cancelled immediately");
             }
-            mLock.notify();
+            mStepConductor.mLock.notify();
         }
     }
 
     /** Notify current vibration that a synced step has completed. */
     public void syncedVibrationComplete() {
-        synchronized (mLock) {
-            if (DEBUG) {
-                Slog.d(TAG, "Synced vibration complete reported by vibrator manager");
-            }
-            for (int i = 0; i < mVibrators.size(); i++) {
-                mStepQueue.notifyVibratorComplete(mVibrators.keyAt(i));
-            }
-            mLock.notify();
-        }
+        mStepConductor.notifySyncedVibrationComplete();
     }
 
     /** Notify current vibration that a step has completed on given vibrator. */
     public void vibratorComplete(int vibratorId) {
-        synchronized (mLock) {
-            if (DEBUG) {
-                Slog.d(TAG, "Vibration complete reported by vibrator " + vibratorId);
-            }
-            mStepQueue.notifyVibratorComplete(vibratorId);
-            mLock.notify();
-        }
+        mStepConductor.notifyVibratorComplete(vibratorId);
     }
 
     // Indicate that the vibration is complete. This can be called multiple times only for
@@ -273,52 +217,55 @@
     private void clientVibrationCompleteIfNotAlready(Vibration.Status completedStatus) {
         if (!mCalledVibrationCompleteCallback) {
             mCalledVibrationCompleteCallback = true;
-            mVibratorManagerHooks.onVibrationCompleted(mVibration.id, completedStatus);
+            mVibratorManagerHooks.onVibrationCompleted(
+                    mStepConductor.getVibration().id, completedStatus);
         }
     }
 
     private void playVibration() {
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
         try {
-            CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffect());
+            CombinedVibration.Sequential sequentialEffect =
+                    toSequential(mStepConductor.getVibration().getEffect());
             final int sequentialEffectSize = sequentialEffect.getEffects().size();
-            mStepQueue.initializeForEffect(sequentialEffect);
+            mStepConductor.initializeForEffect(sequentialEffect);
 
-            while (!mStepQueue.isFinished()) {
+            while (!mStepConductor.isFinished()) {
                 long waitMillisBeforeNextStep;
-                synchronized (mLock) {
-                    waitMillisBeforeNextStep = mStepQueue.getWaitMillisBeforeNextStep();
+                synchronized (mStepConductor.mLock) {
+                    waitMillisBeforeNextStep = mStepConductor.getWaitMillisBeforeNextStepLocked();
                     if (waitMillisBeforeNextStep > 0) {
                         try {
-                            mLock.wait(waitMillisBeforeNextStep);
+                            mStepConductor.mLock.wait(waitMillisBeforeNextStep);
                         } catch (InterruptedException e) {
                         }
                     }
                 }
                 // Only run the next vibration step if we didn't have to wait in this loop.
-                // If we waited then the queue may have changed, so loop again to re-evaluate
-                // the scheduling of the queue top element.
+                // If we waited then the queue may have changed or the wait could have been
+                // interrupted by a cancel call, so loop again to re-evaluate the scheduling of
+                // the queue top element.
                 if (waitMillisBeforeNextStep <= 0) {
                     if (DEBUG) {
                         Slog.d(TAG, "Play vibration consuming next step...");
                     }
                     // Run the step without holding the main lock, to avoid HAL interactions from
                     // blocking the thread.
-                    mStepQueue.runNextStep();
+                    mStepConductor.runNextStep();
                 }
                 Vibration.Status status = mStop ? Vibration.Status.CANCELLED
-                        : mStepQueue.calculateVibrationStatus(sequentialEffectSize);
+                        : mStepConductor.calculateVibrationStatus(sequentialEffectSize);
                 if (status != Vibration.Status.RUNNING && !mCalledVibrationCompleteCallback) {
                     // First time vibration stopped running, start clean-up tasks and notify
                     // callback immediately.
                     clientVibrationCompleteIfNotAlready(status);
                     if (status == Vibration.Status.CANCELLED) {
-                        mStepQueue.cancel();
+                        mStepConductor.cancel();
                     }
                 }
                 if (mForceStop) {
                     // Cancel every step and stop playing them right away, even clean-up steps.
-                    mStepQueue.cancelImmediately();
+                    mStepConductor.cancelImmediately();
                     clientVibrationCompleteIfNotAlready(Vibration.Status.CANCELLED);
                     break;
                 }
@@ -328,61 +275,6 @@
         }
     }
 
-    private void noteVibratorOn(long duration) {
-        try {
-            if (duration <= 0) {
-                return;
-            }
-            if (duration == Long.MAX_VALUE) {
-                // Repeating duration has started. Report a fixed duration here, noteVibratorOff
-                // should be called when this is cancelled.
-                duration = BATTERY_STATS_REPEATING_VIBRATION_DURATION;
-            }
-            mBatteryStatsService.noteVibratorOn(mVibration.uid, duration);
-            FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
-                    mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON,
-                    duration);
-        } catch (RemoteException e) {
-        }
-    }
-
-    private void noteVibratorOff() {
-        try {
-            mBatteryStatsService.noteVibratorOff(mVibration.uid);
-            FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
-                    mVibration.uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF,
-                    /* duration= */ 0);
-        } catch (RemoteException e) {
-        }
-    }
-
-    @Nullable
-    private SingleVibratorStep nextVibrateStep(long startTime, VibratorController controller,
-            VibrationEffect.Composed effect, int segmentIndex, long vibratorOffTimeout) {
-        if (segmentIndex >= effect.getSegments().size()) {
-            segmentIndex = effect.getRepeatIndex();
-        }
-        if (segmentIndex < 0) {
-            // No more segments to play, last step is to complete the vibration on this vibrator.
-            return new EffectCompleteStep(startTime, /* cancelled= */ false, controller,
-                    vibratorOffTimeout);
-        }
-
-        VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
-        if (segment instanceof PrebakedSegment) {
-            return new PerformStep(startTime, controller, effect, segmentIndex, vibratorOffTimeout);
-        }
-        if (segment instanceof PrimitiveSegment) {
-            return new ComposePrimitivesStep(startTime, controller, effect, segmentIndex,
-                    vibratorOffTimeout);
-        }
-        if (segment instanceof RampSegment) {
-            return new ComposePwleStep(startTime, controller, effect, segmentIndex,
-                    vibratorOffTimeout);
-        }
-        return new AmplitudeStep(startTime, controller, effect, segmentIndex, vibratorOffTimeout);
-    }
-
     private static CombinedVibration.Sequential toSequential(CombinedVibration effect) {
         if (effect instanceof CombinedVibration.Sequential) {
             return (CombinedVibration.Sequential) effect;
@@ -392,1268 +284,4 @@
                 .combine();
     }
 
-    /** Queue for {@link Step Steps}, sorted by their start time. */
-    private final class StepQueue {
-        @GuardedBy("mLock")
-        private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>();
-        @GuardedBy("mLock")
-        private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>();
-        @GuardedBy("mLock")
-        private final Queue<Integer> mCompletionNotifiedVibrators = new LinkedList<>();
-
-        @GuardedBy("mLock")
-        private int mPendingVibrateSteps;
-        @GuardedBy("mLock")
-        private int mConsumedStartVibrateSteps;
-        @GuardedBy("mLock")
-        private int mSuccessfulVibratorOnSteps;
-        @GuardedBy("mLock")
-        private boolean mWaitToProcessVibratorCompleteCallbacks;
-
-        public void initializeForEffect(@NonNull CombinedVibration.Sequential vibration) {
-            synchronized (mLock) {
-                mPendingVibrateSteps++;
-                mNextSteps.offer(new StartVibrateStep(vibration));
-            }
-        }
-
-        public boolean isFinished() {
-            synchronized (mLock) {
-                return mPendingOnVibratorCompleteSteps.isEmpty() && mNextSteps.isEmpty();
-            }
-        }
-
-        /**
-         * Calculate the {@link Vibration.Status} based on the current queue state and the expected
-         * number of {@link StartVibrateStep} to be played.
-         */
-        public Vibration.Status calculateVibrationStatus(int expectedStartVibrateSteps) {
-            synchronized (mLock) {
-                if (mPendingVibrateSteps > 0
-                        || mConsumedStartVibrateSteps < expectedStartVibrateSteps) {
-                    return Vibration.Status.RUNNING;
-                }
-                if (mSuccessfulVibratorOnSteps > 0) {
-                    return Vibration.Status.FINISHED;
-                }
-                // If no step was able to turn the vibrator ON successfully.
-                return Vibration.Status.IGNORED_UNSUPPORTED;
-            }
-        }
-
-        /** Returns the time in millis to wait before calling {@link #runNextStep()}. */
-        @GuardedBy("VibrationThread.this.mLock")
-        public long getWaitMillisBeforeNextStep() {
-            if (!mPendingOnVibratorCompleteSteps.isEmpty()) {
-                // Steps resumed by vibrator complete callback should be played right away.
-                return 0;
-            }
-            Step nextStep = mNextSteps.peek();
-            return nextStep == null ? 0 : nextStep.calculateWaitTime();
-        }
-
-        /**
-         * Play and remove the step at the top of this queue, and also adds the next steps generated
-         * to be played next.
-         */
-        public void runNextStep() {
-            // Vibrator callbacks should wait until the polled step is played and the next steps are
-            // added back to the queue, so they can handle the callback.
-            markWaitToProcessVibratorCallbacks();
-            try {
-                Step nextStep = pollNext();
-                if (nextStep != null) {
-                    // This might turn on the vibrator and have a HAL latency. Execute this outside
-                    // any lock to avoid blocking other interactions with the thread.
-                    List<Step> nextSteps = nextStep.play();
-                    synchronized (mLock) {
-                        if (nextStep.getVibratorOnDuration() > 0) {
-                            mSuccessfulVibratorOnSteps++;
-                        }
-                        if (nextStep instanceof StartVibrateStep) {
-                            mConsumedStartVibrateSteps++;
-                        }
-                        if (!nextStep.isCleanUp()) {
-                            mPendingVibrateSteps--;
-                        }
-                        for (int i = 0; i < nextSteps.size(); i++) {
-                            mPendingVibrateSteps += nextSteps.get(i).isCleanUp() ? 0 : 1;
-                        }
-                        mNextSteps.addAll(nextSteps);
-                    }
-                }
-            } finally {
-                synchronized (mLock) {
-                    processVibratorCompleteCallbacks();
-                }
-            }
-        }
-
-        /**
-         * Notify the vibrator completion.
-         *
-         * <p>This is a lightweight method that do not trigger any operation from {@link
-         * VibratorController}, so it can be called directly from a native callback.
-         */
-        @GuardedBy("mLock")
-        public void notifyVibratorComplete(int vibratorId) {
-            mCompletionNotifiedVibrators.offer(vibratorId);
-            if (!mWaitToProcessVibratorCompleteCallbacks) {
-                // No step is being played or cancelled now, process the callback right away.
-                processVibratorCompleteCallbacks();
-            }
-        }
-
-        /**
-         * Cancel the current queue, replacing all remaining steps with respective clean-up steps.
-         *
-         * <p>This will remove all steps and replace them with respective
-         * {@link Step#cancel()}.
-         */
-        public void cancel() {
-            // Vibrator callbacks should wait until all steps from the queue are properly cancelled
-            // and clean up steps are added back to the queue, so they can handle the callback.
-            markWaitToProcessVibratorCallbacks();
-            try {
-                List<Step> cleanUpSteps = new ArrayList<>();
-                Step step;
-                while ((step = pollNext()) != null) {
-                    cleanUpSteps.addAll(step.cancel());
-                }
-                synchronized (mLock) {
-                    // All steps generated by Step.cancel() should be clean-up steps.
-                    mPendingVibrateSteps = 0;
-                    mNextSteps.addAll(cleanUpSteps);
-                }
-            } finally {
-                synchronized (mLock) {
-                    processVibratorCompleteCallbacks();
-                }
-            }
-        }
-
-        /**
-         * Cancel the current queue immediately, clearing all remaining steps and skipping clean-up.
-         *
-         * <p>This will remove and trigger {@link Step#cancelImmediately()} in all steps, in order.
-         */
-        public void cancelImmediately() {
-            // Vibrator callbacks should wait until all steps from the queue are properly cancelled.
-            markWaitToProcessVibratorCallbacks();
-            try {
-                Step step;
-                while ((step = pollNext()) != null) {
-                    // This might turn off the vibrator and have a HAL latency. Execute this outside
-                    // any lock to avoid blocking other interactions with the thread.
-                    step.cancelImmediately();
-                }
-                synchronized (mLock) {
-                    mPendingVibrateSteps = 0;
-                }
-            } finally {
-                synchronized (mLock) {
-                    processVibratorCompleteCallbacks();
-                }
-            }
-        }
-
-        @Nullable
-        private Step pollNext() {
-            synchronized (mLock) {
-                // Prioritize the steps resumed by a vibrator complete callback.
-                if (!mPendingOnVibratorCompleteSteps.isEmpty()) {
-                    return mPendingOnVibratorCompleteSteps.poll();
-                }
-                return mNextSteps.poll();
-            }
-        }
-
-        private void markWaitToProcessVibratorCallbacks() {
-            synchronized (mLock) {
-                mWaitToProcessVibratorCompleteCallbacks = true;
-            }
-        }
-
-        /**
-         * Notify the step in this queue that should be resumed by the vibrator completion
-         * callback and keep it separate to be consumed by {@link #runNextStep()}.
-         *
-         * <p>This is a lightweight method that do not trigger any operation from {@link
-         * VibratorController}, so it can be called directly from a native callback.
-         *
-         * <p>This assumes only one of the next steps is waiting on this given vibrator, so the
-         * first step found will be resumed by this method, in no particular order.
-         */
-        @GuardedBy("mLock")
-        private void processVibratorCompleteCallbacks() {
-            mWaitToProcessVibratorCompleteCallbacks = false;
-            while (!mCompletionNotifiedVibrators.isEmpty()) {
-                int vibratorId = mCompletionNotifiedVibrators.poll();
-                Iterator<Step> it = mNextSteps.iterator();
-                while (it.hasNext()) {
-                    Step step = it.next();
-                    if (step.acceptVibratorCompleteCallback(vibratorId)) {
-                        it.remove();
-                        mPendingOnVibratorCompleteSteps.offer(step);
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Represent a single step for playing a vibration.
-     *
-     * <p>Every step has a start time, which can be used to apply delays between steps while
-     * executing them in sequence.
-     */
-    private abstract class Step implements Comparable<Step> {
-        public final long startTime;
-
-        Step(long startTime) {
-            this.startTime = startTime;
-        }
-
-        /**
-         * Returns true if this step is a clean up step and not part of a {@link VibrationEffect} or
-         * {@link CombinedVibration}.
-         */
-        public boolean isCleanUp() {
-            return false;
-        }
-
-        /** Play this step, returning a (possibly empty) list of next steps. */
-        @NonNull
-        public abstract List<Step> play();
-
-        /**
-         * Cancel this pending step and return a (possibly empty) list of clean-up steps that should
-         * be played to gracefully cancel this step.
-         */
-        @NonNull
-        public abstract List<Step> cancel();
-
-        /** Cancel this pending step immediately, skipping any clean-up. */
-        public abstract void cancelImmediately();
-
-        /**
-         * Return the duration the vibrator was turned on when this step was played.
-         *
-         * @return A positive duration that the vibrator was turned on for by this step;
-         * Zero if the segment is not supported, the step was not played yet or vibrator was never
-         * turned on by this step; A negative value if the vibrator call has failed.
-         */
-        public long getVibratorOnDuration() {
-            return 0;
-        }
-
-        /**
-         * Return true to run this step right after a vibrator has notified vibration completed,
-         * used to resume steps waiting on vibrator callbacks with a timeout.
-         */
-        public boolean acceptVibratorCompleteCallback(int vibratorId) {
-            return false;
-        }
-
-        /**
-         * Returns the time in millis to wait before playing this step. This is performed
-         * while holding the queue lock, so should not rely on potentially slow operations.
-         */
-        public long calculateWaitTime() {
-            if (startTime == Long.MAX_VALUE) {
-                // This step don't have a predefined start time, it's just marked to be executed
-                // after all other steps have finished.
-                return 0;
-            }
-            return Math.max(0, startTime - SystemClock.uptimeMillis());
-        }
-
-        @Override
-        public int compareTo(Step o) {
-            return Long.compare(startTime, o.startTime);
-        }
-    }
-
-    /**
-     * Starts a sync vibration.
-     *
-     * <p>If this step has successfully started playing a vibration on any vibrator, it will always
-     * add a {@link FinishVibrateStep} to the queue, to be played after all vibrators have finished
-     * all their individual steps.
-     *
-     * <p>If this step does not start any vibrator, it will add a {@link StartVibrateStep} if the
-     * sequential effect isn't finished yet.
-     */
-    private final class StartVibrateStep extends Step {
-        public final CombinedVibration.Sequential sequentialEffect;
-        public final int currentIndex;
-
-        private long mVibratorsOnMaxDuration;
-
-        StartVibrateStep(CombinedVibration.Sequential effect) {
-            this(SystemClock.uptimeMillis() + effect.getDelays().get(0), effect, /* index= */ 0);
-        }
-
-        StartVibrateStep(long startTime, CombinedVibration.Sequential effect, int index) {
-            super(startTime);
-            sequentialEffect = effect;
-            currentIndex = index;
-        }
-
-        @Override
-        public long getVibratorOnDuration() {
-            return mVibratorsOnMaxDuration;
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "StartVibrateStep");
-            List<Step> nextSteps = new ArrayList<>();
-            mVibratorsOnMaxDuration = -1;
-            try {
-                if (DEBUG) {
-                    Slog.d(TAG, "StartVibrateStep for effect #" + currentIndex);
-                }
-                CombinedVibration effect = sequentialEffect.getEffects().get(currentIndex);
-                DeviceEffectMap effectMapping = createEffectToVibratorMapping(effect);
-                if (effectMapping == null) {
-                    // Unable to map effects to vibrators, ignore this step.
-                    return nextSteps;
-                }
-
-                mVibratorsOnMaxDuration = startVibrating(effectMapping, nextSteps);
-                noteVibratorOn(mVibratorsOnMaxDuration);
-            } finally {
-                if (mVibratorsOnMaxDuration >= 0) {
-                    // It least one vibrator was started then add a finish step to wait for all
-                    // active vibrators to finish their individual steps before going to the next.
-                    // Otherwise this step was ignored so just go to the next one.
-                    Step nextStep =
-                            mVibratorsOnMaxDuration > 0 ? new FinishVibrateStep(this) : nextStep();
-                    if (nextStep != null) {
-                        nextSteps.add(nextStep);
-                    }
-                }
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-            return nextSteps;
-        }
-
-        @Override
-        public List<Step> cancel() {
-            return EMPTY_STEP_LIST;
-        }
-
-        @Override
-        public void cancelImmediately() {
-        }
-
-        /**
-         * Create the next {@link StartVibrateStep} to play this sequential effect, starting at the
-         * time this method is called, or null if sequence is complete.
-         */
-        @Nullable
-        private Step nextStep() {
-            int nextIndex = currentIndex + 1;
-            if (nextIndex >= sequentialEffect.getEffects().size()) {
-                return null;
-            }
-            long nextEffectDelay = sequentialEffect.getDelays().get(nextIndex);
-            long nextStartTime = SystemClock.uptimeMillis() + nextEffectDelay;
-            return new StartVibrateStep(nextStartTime, sequentialEffect, nextIndex);
-        }
-
-        /** Create a mapping of individual {@link VibrationEffect} to available vibrators. */
-        @Nullable
-        private DeviceEffectMap createEffectToVibratorMapping(
-                CombinedVibration effect) {
-            if (effect instanceof CombinedVibration.Mono) {
-                return new DeviceEffectMap((CombinedVibration.Mono) effect);
-            }
-            if (effect instanceof CombinedVibration.Stereo) {
-                return new DeviceEffectMap((CombinedVibration.Stereo) effect);
-            }
-            return null;
-        }
-
-        /**
-         * Starts playing effects on designated vibrators, in sync.
-         *
-         * @param effectMapping The {@link CombinedVibration} mapped to this device vibrators
-         * @param nextSteps     An output list to accumulate the future {@link Step Steps} created
-         *                      by this method, typically one for each vibrator that has
-         *                      successfully started vibrating on this step.
-         * @return The duration, in millis, of the {@link CombinedVibration}. Repeating
-         * waveforms return {@link Long#MAX_VALUE}. Zero or negative values indicate the vibrators
-         * have ignored all effects.
-         */
-        private long startVibrating(DeviceEffectMap effectMapping, List<Step> nextSteps) {
-            int vibratorCount = effectMapping.size();
-            if (vibratorCount == 0) {
-                // No effect was mapped to any available vibrator.
-                return 0;
-            }
-
-            SingleVibratorStep[] steps = new SingleVibratorStep[vibratorCount];
-            long vibrationStartTime = SystemClock.uptimeMillis();
-            for (int i = 0; i < vibratorCount; i++) {
-                steps[i] = nextVibrateStep(vibrationStartTime,
-                        mVibrators.get(effectMapping.vibratorIdAt(i)),
-                        effectMapping.effectAt(i),
-                        /* segmentIndex= */ 0, /* vibratorOffTimeout= */ 0);
-            }
-
-            if (steps.length == 1) {
-                // No need to prepare and trigger sync effects on a single vibrator.
-                return startVibrating(steps[0], nextSteps);
-            }
-
-            // This synchronization of vibrators should be executed one at a time, even if we are
-            // vibrating different sets of vibrators in parallel. The manager can only prepareSynced
-            // one set of vibrators at a time.
-            synchronized (mLock) {
-                boolean hasPrepared = false;
-                boolean hasTriggered = false;
-                long maxDuration = 0;
-                try {
-                    hasPrepared = mVibratorManagerHooks.prepareSyncedVibration(
-                            effectMapping.getRequiredSyncCapabilities(),
-                            effectMapping.getVibratorIds());
-
-                    for (SingleVibratorStep step : steps) {
-                        long duration = startVibrating(step, nextSteps);
-                        if (duration < 0) {
-                            // One vibrator has failed, fail this entire sync attempt.
-                            return maxDuration = -1;
-                        }
-                        maxDuration = Math.max(maxDuration, duration);
-                    }
-
-                    // Check if sync was prepared and if any step was accepted by a vibrator,
-                    // otherwise there is nothing to trigger here.
-                    if (hasPrepared && maxDuration > 0) {
-                        hasTriggered = mVibratorManagerHooks.triggerSyncedVibration(mVibration.id);
-                    }
-                    return maxDuration;
-                } finally {
-                    if (hasPrepared && !hasTriggered) {
-                        // Trigger has failed or all steps were ignored by the vibrators.
-                        mVibratorManagerHooks.cancelSyncedVibration();
-                        nextSteps.clear();
-                    } else if (maxDuration < 0) {
-                        // Some vibrator failed without being prepared so other vibrators might be
-                        // active. Cancel and remove every pending step from output list.
-                        for (int i = nextSteps.size() - 1; i >= 0; i--) {
-                            nextSteps.remove(i).cancelImmediately();
-                        }
-                    }
-                }
-            }
-        }
-
-        private long startVibrating(SingleVibratorStep step, List<Step> nextSteps) {
-            nextSteps.addAll(step.play());
-            long stepDuration = step.getVibratorOnDuration();
-            if (stepDuration < 0) {
-                // Step failed, so return negative duration to propagate failure.
-                return stepDuration;
-            }
-            // Return the longest estimation for the entire effect.
-            return Math.max(stepDuration, step.effect.getDuration());
-        }
-    }
-
-    /**
-     * Finish a sync vibration started by a {@link StartVibrateStep}.
-     *
-     * <p>This only plays after all active vibrators steps have finished, and adds a {@link
-     * StartVibrateStep} to the queue if the sequential effect isn't finished yet.
-     */
-    private final class FinishVibrateStep extends Step {
-        public final StartVibrateStep startedStep;
-
-        FinishVibrateStep(StartVibrateStep startedStep) {
-            super(Long.MAX_VALUE); // No predefined startTime, just wait for all steps in the queue.
-            this.startedStep = startedStep;
-        }
-
-        @Override
-        public boolean isCleanUp() {
-            // This step only notes that all the vibrators has been turned off.
-            return true;
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "FinishVibrateStep");
-            try {
-                if (DEBUG) {
-                    Slog.d(TAG, "FinishVibrateStep for effect #" + startedStep.currentIndex);
-                }
-                noteVibratorOff();
-                Step nextStep = startedStep.nextStep();
-                return nextStep == null ? EMPTY_STEP_LIST : Arrays.asList(nextStep);
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-
-        @Override
-        public List<Step> cancel() {
-            cancelImmediately();
-            return EMPTY_STEP_LIST;
-        }
-
-        @Override
-        public void cancelImmediately() {
-            noteVibratorOff();
-        }
-    }
-
-    /**
-     * Represent a step on a single vibrator that plays one or more segments from a
-     * {@link VibrationEffect.Composed} effect.
-     */
-    private abstract class SingleVibratorStep extends Step {
-        public final VibratorController controller;
-        public final VibrationEffect.Composed effect;
-        public final int segmentIndex;
-        public final long vibratorOffTimeout;
-
-        long mVibratorOnResult;
-        boolean mVibratorCompleteCallbackReceived;
-
-        /**
-         * @param startTime          The time to schedule this step in the {@link StepQueue}.
-         * @param controller         The vibrator that is playing the effect.
-         * @param effect             The effect being played in this step.
-         * @param index              The index of the next segment to be played by this step
-         * @param vibratorOffTimeout The time the vibrator is expected to complete any previous
-         *                           vibration and turn off. This is used to allow this step to be
-         *                           triggered when the completion callback is received, and can
-         *                           be used play effects back-to-back.
-         */
-        SingleVibratorStep(long startTime, VibratorController controller,
-                VibrationEffect.Composed effect, int index, long vibratorOffTimeout) {
-            super(startTime);
-            this.controller = controller;
-            this.effect = effect;
-            this.segmentIndex = index;
-            this.vibratorOffTimeout = vibratorOffTimeout;
-        }
-
-        @Override
-        public long getVibratorOnDuration() {
-            return mVibratorOnResult;
-        }
-
-        @Override
-        public boolean acceptVibratorCompleteCallback(int vibratorId) {
-            boolean isSameVibrator = controller.getVibratorInfo().getId() == vibratorId;
-            mVibratorCompleteCallbackReceived |= isSameVibrator;
-            // Only activate this step if a timeout was set to wait for the vibration to complete,
-            // otherwise we are waiting for the correct time to play the next step.
-            return isSameVibrator && (vibratorOffTimeout > SystemClock.uptimeMillis());
-        }
-
-        @Override
-        public List<Step> cancel() {
-            return Arrays.asList(new EffectCompleteStep(SystemClock.uptimeMillis(),
-                    /* cancelled= */ true, controller, vibratorOffTimeout));
-        }
-
-        @Override
-        public void cancelImmediately() {
-            if (vibratorOffTimeout > SystemClock.uptimeMillis()) {
-                // Vibrator might be running from previous steps, so turn it off while canceling.
-                stopVibrating();
-            }
-        }
-
-        void stopVibrating() {
-            if (DEBUG) {
-                Slog.d(TAG, "Turning off vibrator " + controller.getVibratorInfo().getId());
-            }
-            controller.off();
-        }
-
-        void changeAmplitude(float amplitude) {
-            if (DEBUG) {
-                Slog.d(TAG, "Amplitude changed on vibrator " + controller.getVibratorInfo().getId()
-                        + " to " + amplitude);
-            }
-            controller.setAmplitude(amplitude);
-        }
-
-        /** Return the {@link #nextVibrateStep} with same timings, only jumping the segments. */
-        public List<Step> skipToNextSteps(int segmentsSkipped) {
-            return nextSteps(startTime, vibratorOffTimeout, segmentsSkipped);
-        }
-
-        /**
-         * Return the {@link #nextVibrateStep} with same start and off timings calculated from
-         * {@link #getVibratorOnDuration()}, jumping all played segments.
-         *
-         * <p>This method has same behavior as {@link #skipToNextSteps(int)} when the vibrator
-         * result is non-positive, meaning the vibrator has either ignored or failed to turn on.
-         */
-        public List<Step> nextSteps(int segmentsPlayed) {
-            if (mVibratorOnResult <= 0) {
-                // Vibration was not started, so just skip the played segments and keep timings.
-                return skipToNextSteps(segmentsPlayed);
-            }
-            long nextStartTime = SystemClock.uptimeMillis() + mVibratorOnResult;
-            long nextVibratorOffTimeout = nextStartTime + CALLBACKS_EXTRA_TIMEOUT;
-            return nextSteps(nextStartTime, nextVibratorOffTimeout, segmentsPlayed);
-        }
-
-        /**
-         * Return the {@link #nextVibrateStep} with given start and off timings, which might be
-         * calculated independently, jumping all played segments.
-         *
-         * <p>This should be used when the vibrator on/off state is not responsible for the steps
-         * execution timings, e.g. while playing the vibrator amplitudes.
-         */
-        public List<Step> nextSteps(long nextStartTime, long vibratorOffTimeout,
-                int segmentsPlayed) {
-            Step nextStep = nextVibrateStep(nextStartTime, controller, effect,
-                    segmentIndex + segmentsPlayed, vibratorOffTimeout);
-            return nextStep == null ? EMPTY_STEP_LIST : Arrays.asList(nextStep);
-        }
-    }
-
-    /**
-     * Represent a step turn the vibrator on with a single prebaked effect.
-     *
-     * <p>This step automatically falls back by replacing the prebaked segment with
-     * {@link VibrationSettings#getFallbackEffect(int)}, if available.
-     */
-    private final class PerformStep extends SingleVibratorStep {
-
-        PerformStep(long startTime, VibratorController controller,
-                VibrationEffect.Composed effect, int index, long vibratorOffTimeout) {
-            // This step should wait for the last vibration to finish (with the timeout) and for the
-            // intended step start time (to respect the effect delays).
-            super(Math.max(startTime, vibratorOffTimeout), controller, effect, index,
-                    vibratorOffTimeout);
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformStep");
-            try {
-                VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
-                if (!(segment instanceof PrebakedSegment)) {
-                    Slog.w(TAG, "Ignoring wrong segment for a PerformStep: " + segment);
-                    return skipToNextSteps(/* segmentsSkipped= */ 1);
-                }
-
-                PrebakedSegment prebaked = (PrebakedSegment) segment;
-                if (DEBUG) {
-                    Slog.d(TAG, "Perform " + VibrationEffect.effectIdToString(
-                            prebaked.getEffectId()) + " on vibrator "
-                            + controller.getVibratorInfo().getId());
-                }
-
-                VibrationEffect fallback = mVibration.getFallback(prebaked.getEffectId());
-                mVibratorOnResult = controller.on(prebaked, mVibration.id);
-
-                if (mVibratorOnResult == 0 && prebaked.shouldFallback()
-                        && (fallback instanceof VibrationEffect.Composed)) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "Playing fallback for effect "
-                                + VibrationEffect.effectIdToString(prebaked.getEffectId()));
-                    }
-                    SingleVibratorStep fallbackStep = nextVibrateStep(startTime, controller,
-                            replaceCurrentSegment((VibrationEffect.Composed) fallback),
-                            segmentIndex, vibratorOffTimeout);
-                    List<Step> fallbackResult = fallbackStep.play();
-                    // Update the result with the fallback result so this step is seamlessly
-                    // replaced by the fallback to any outer application of this.
-                    mVibratorOnResult = fallbackStep.getVibratorOnDuration();
-                    return fallbackResult;
-                }
-
-                return nextSteps(/* segmentsPlayed= */ 1);
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-
-        /**
-         * Replace segment at {@link #segmentIndex} in {@link #effect} with given fallback segments.
-         *
-         * @return a copy of {@link #effect} with replaced segment.
-         */
-        private VibrationEffect.Composed replaceCurrentSegment(VibrationEffect.Composed fallback) {
-            List<VibrationEffectSegment> newSegments = new ArrayList<>(effect.getSegments());
-            int newRepeatIndex = effect.getRepeatIndex();
-            newSegments.remove(segmentIndex);
-            newSegments.addAll(segmentIndex, fallback.getSegments());
-            if (segmentIndex < effect.getRepeatIndex()) {
-                newRepeatIndex += fallback.getSegments().size() - 1;
-            }
-            return new VibrationEffect.Composed(newSegments, newRepeatIndex);
-        }
-    }
-
-    /**
-     * Represent a step turn the vibrator on using a composition of primitives.
-     *
-     * <p>This step will use the maximum supported number of consecutive segments of type
-     * {@link PrimitiveSegment} starting at the current index.
-     */
-    private final class ComposePrimitivesStep extends SingleVibratorStep {
-
-        ComposePrimitivesStep(long startTime, VibratorController controller,
-                VibrationEffect.Composed effect, int index, long vibratorOffTimeout) {
-            // This step should wait for the last vibration to finish (with the timeout) and for the
-            // intended step start time (to respect the effect delays).
-            super(Math.max(startTime, vibratorOffTimeout), controller, effect, index,
-                    vibratorOffTimeout);
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePrimitivesStep");
-            try {
-                // Load the next PrimitiveSegments to create a single compose call to the vibrator,
-                // limited to the vibrator composition maximum size.
-                int limit = controller.getVibratorInfo().getCompositionSizeMax();
-                int segmentCount = limit > 0
-                        ? Math.min(effect.getSegments().size(), segmentIndex + limit)
-                        : effect.getSegments().size();
-                List<PrimitiveSegment> primitives = new ArrayList<>();
-                for (int i = segmentIndex; i < segmentCount; i++) {
-                    VibrationEffectSegment segment = effect.getSegments().get(i);
-                    if (segment instanceof PrimitiveSegment) {
-                        primitives.add((PrimitiveSegment) segment);
-                    } else {
-                        break;
-                    }
-                }
-
-                if (primitives.isEmpty()) {
-                    Slog.w(TAG, "Ignoring wrong segment for a ComposePrimitivesStep: "
-                            + effect.getSegments().get(segmentIndex));
-                    return skipToNextSteps(/* segmentsSkipped= */ 1);
-                }
-
-                if (DEBUG) {
-                    Slog.d(TAG, "Compose " + primitives + " primitives on vibrator "
-                            + controller.getVibratorInfo().getId());
-                }
-                mVibratorOnResult = controller.on(
-                        primitives.toArray(new PrimitiveSegment[primitives.size()]),
-                        mVibration.id);
-
-                return nextSteps(/* segmentsPlayed= */ primitives.size());
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-    }
-
-    /**
-     * Represent a step turn the vibrator on using a composition of PWLE segments.
-     *
-     * <p>This step will use the maximum supported number of consecutive segments of type
-     * {@link StepSegment} or {@link RampSegment} starting at the current index.
-     */
-    private final class ComposePwleStep extends SingleVibratorStep {
-
-        ComposePwleStep(long startTime, VibratorController controller,
-                VibrationEffect.Composed effect, int index, long vibratorOffTimeout) {
-            // This step should wait for the last vibration to finish (with the timeout) and for the
-            // intended step start time (to respect the effect delays).
-            super(Math.max(startTime, vibratorOffTimeout), controller, effect, index,
-                    vibratorOffTimeout);
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleStep");
-            try {
-                // Load the next RampSegments to create a single composePwle call to the vibrator,
-                // limited to the vibrator PWLE maximum size.
-                int limit = controller.getVibratorInfo().getPwleSizeMax();
-                int segmentCount = limit > 0
-                        ? Math.min(effect.getSegments().size(), segmentIndex + limit)
-                        : effect.getSegments().size();
-                List<RampSegment> pwles = new ArrayList<>();
-                for (int i = segmentIndex; i < segmentCount; i++) {
-                    VibrationEffectSegment segment = effect.getSegments().get(i);
-                    if (segment instanceof RampSegment) {
-                        pwles.add((RampSegment) segment);
-                    } else {
-                        break;
-                    }
-                }
-
-                if (pwles.isEmpty()) {
-                    Slog.w(TAG, "Ignoring wrong segment for a ComposePwleStep: "
-                            + effect.getSegments().get(segmentIndex));
-                    return skipToNextSteps(/* segmentsSkipped= */ 1);
-                }
-
-                if (DEBUG) {
-                    Slog.d(TAG, "Compose " + pwles + " PWLEs on vibrator "
-                            + controller.getVibratorInfo().getId());
-                }
-                mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]),
-                        mVibration.id);
-
-                return nextSteps(/* segmentsPlayed= */ pwles.size());
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-    }
-
-    /**
-     * Represents a step to complete a {@link VibrationEffect}.
-     *
-     * <p>This runs right at the time the vibration is considered to end and will update the pending
-     * vibrators count. This can turn off the vibrator or slowly ramp it down to zero amplitude.
-     */
-    private final class EffectCompleteStep extends SingleVibratorStep {
-        private final boolean mCancelled;
-
-        EffectCompleteStep(long startTime, boolean cancelled, VibratorController controller,
-                long vibratorOffTimeout) {
-            super(startTime, controller, /* effect= */ null, /* index= */ -1, vibratorOffTimeout);
-            mCancelled = cancelled;
-        }
-
-        @Override
-        public boolean isCleanUp() {
-            // If the vibration was cancelled then this is just a clean up to ramp off the vibrator.
-            // Otherwise this step is part of the vibration.
-            return mCancelled;
-        }
-
-        @Override
-        public List<Step> cancel() {
-            if (mCancelled) {
-                // Double cancelling will just turn off the vibrator right away.
-                return Arrays.asList(new OffStep(SystemClock.uptimeMillis(), controller));
-            }
-            return super.cancel();
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "EffectCompleteStep");
-            try {
-                if (DEBUG) {
-                    Slog.d(TAG, "Running " + (mCancelled ? "cancel" : "complete") + " vibration"
-                            + " step on vibrator " + controller.getVibratorInfo().getId());
-                }
-                if (mVibratorCompleteCallbackReceived) {
-                    // Vibration completion callback was received by this step, just turn if off
-                    // and skip any clean-up.
-                    stopVibrating();
-                    return EMPTY_STEP_LIST;
-                }
-
-                float currentAmplitude = controller.getCurrentAmplitude();
-                long remainingOnDuration =
-                        vibratorOffTimeout - CALLBACKS_EXTRA_TIMEOUT - SystemClock.uptimeMillis();
-                long rampDownDuration =
-                        Math.min(remainingOnDuration, mVibrationSettings.getRampDownDuration());
-                long stepDownDuration = mVibrationSettings.getRampStepDuration();
-                if (currentAmplitude < RAMP_OFF_AMPLITUDE_MIN
-                        || rampDownDuration <= stepDownDuration) {
-                    // No need to ramp down the amplitude, just wait to turn it off.
-                    if (mCancelled) {
-                        // Vibration is completing because it was cancelled, turn off right away.
-                        stopVibrating();
-                        return EMPTY_STEP_LIST;
-                    } else {
-                        return Arrays.asList(new OffStep(vibratorOffTimeout, controller));
-                    }
-                }
-
-                if (DEBUG) {
-                    Slog.d(TAG, "Ramping down vibrator " + controller.getVibratorInfo().getId()
-                            + " from amplitude " + currentAmplitude
-                            + " for " + rampDownDuration + "ms");
-                }
-                float amplitudeDelta = currentAmplitude / (rampDownDuration / stepDownDuration);
-                float amplitudeTarget = currentAmplitude - amplitudeDelta;
-                long newVibratorOffTimeout = mCancelled ? rampDownDuration : vibratorOffTimeout;
-                return Arrays.asList(new RampOffStep(startTime, amplitudeTarget, amplitudeDelta,
-                        controller, newVibratorOffTimeout));
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-    }
-
-    /** Represents a step to ramp down the vibrator amplitude before turning it off. */
-    private final class RampOffStep extends SingleVibratorStep {
-        private final float mAmplitudeTarget;
-        private final float mAmplitudeDelta;
-
-        RampOffStep(long startTime, float amplitudeTarget, float amplitudeDelta,
-                VibratorController controller, long vibratorOffTimeout) {
-            super(startTime, controller, /* effect= */ null, /* index= */ -1, vibratorOffTimeout);
-            mAmplitudeTarget = amplitudeTarget;
-            mAmplitudeDelta = amplitudeDelta;
-        }
-
-        @Override
-        public boolean isCleanUp() {
-            return true;
-        }
-
-        @Override
-        public List<Step> cancel() {
-            return Arrays.asList(new OffStep(SystemClock.uptimeMillis(), controller));
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "RampOffStep");
-            try {
-                if (DEBUG) {
-                    long latency = SystemClock.uptimeMillis() - startTime;
-                    Slog.d(TAG, "Ramp down the vibrator amplitude, step with "
-                            + latency + "ms latency.");
-                }
-                if (mVibratorCompleteCallbackReceived) {
-                    // Vibration completion callback was received by this step, just turn if off
-                    // and skip the rest of the steps to ramp down the vibrator amplitude.
-                    stopVibrating();
-                    return EMPTY_STEP_LIST;
-                }
-
-                changeAmplitude(mAmplitudeTarget);
-
-                float newAmplitudeTarget = mAmplitudeTarget - mAmplitudeDelta;
-                if (newAmplitudeTarget < RAMP_OFF_AMPLITUDE_MIN) {
-                    // Vibrator amplitude cannot go further down, just turn it off.
-                    return Arrays.asList(new OffStep(vibratorOffTimeout, controller));
-                }
-                return Arrays.asList(new RampOffStep(
-                        startTime + mVibrationSettings.getRampStepDuration(), newAmplitudeTarget,
-                        mAmplitudeDelta, controller, vibratorOffTimeout));
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-    }
-
-    /**
-     * Represents a step to turn the vibrator off.
-     *
-     * <p>This runs after a timeout on the expected time the vibrator should have finished playing,
-     * and can be brought forward by vibrator complete callbacks.
-     */
-    private final class OffStep extends SingleVibratorStep {
-
-        OffStep(long startTime, VibratorController controller) {
-            super(startTime, controller, /* effect= */ null, /* index= */ -1, startTime);
-        }
-
-        @Override
-        public boolean isCleanUp() {
-            return true;
-        }
-
-        @Override
-        public List<Step> cancel() {
-            return Arrays.asList(new OffStep(SystemClock.uptimeMillis(), controller));
-        }
-
-        @Override
-        public void cancelImmediately() {
-            stopVibrating();
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "OffStep");
-            try {
-                stopVibrating();
-                return EMPTY_STEP_LIST;
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-    }
-
-    /**
-     * Represents a step to turn the vibrator on and change its amplitude.
-     *
-     * <p>This step ignores vibration completion callbacks and control the vibrator on/off state
-     * and amplitude to simulate waveforms represented by a sequence of {@link StepSegment}.
-     */
-    private final class AmplitudeStep extends SingleVibratorStep {
-        private long mNextOffTime;
-
-        AmplitudeStep(long startTime, VibratorController controller,
-                VibrationEffect.Composed effect, int index, long vibratorOffTimeout) {
-            // This step has a fixed startTime coming from the timings of the waveform it's playing.
-            super(startTime, controller, effect, index, vibratorOffTimeout);
-            mNextOffTime = vibratorOffTimeout;
-        }
-
-        @Override
-        public boolean acceptVibratorCompleteCallback(int vibratorId) {
-            if (controller.getVibratorInfo().getId() == vibratorId) {
-                mVibratorCompleteCallbackReceived = true;
-                mNextOffTime = SystemClock.uptimeMillis();
-            }
-            // Timings are tightly controlled here, so only trigger this step if the vibrator was
-            // supposed to be ON but has completed prematurely, to turn it back on as soon as
-            // possible.
-            return mNextOffTime < startTime && controller.getCurrentAmplitude() > 0;
-        }
-
-        @Override
-        public List<Step> play() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "AmplitudeStep");
-            try {
-                long now = SystemClock.uptimeMillis();
-                long latency = now - startTime;
-                if (DEBUG) {
-                    Slog.d(TAG, "Running amplitude step with " + latency + "ms latency.");
-                }
-
-                if (mVibratorCompleteCallbackReceived && latency < 0) {
-                    // This step was run early because the vibrator turned off prematurely.
-                    // Turn it back on and return this same step to run at the exact right time.
-                    mNextOffTime = turnVibratorBackOn(/* remainingDuration= */ -latency);
-                    return Arrays.asList(new AmplitudeStep(startTime, controller, effect,
-                            segmentIndex, mNextOffTime));
-                }
-
-                VibrationEffectSegment segment = effect.getSegments().get(segmentIndex);
-                if (!(segment instanceof StepSegment)) {
-                    Slog.w(TAG, "Ignoring wrong segment for a AmplitudeStep: " + segment);
-                    return skipToNextSteps(/* segmentsSkipped= */ 1);
-                }
-
-                StepSegment stepSegment = (StepSegment) segment;
-                if (stepSegment.getDuration() == 0) {
-                    // Skip waveform entries with zero timing.
-                    return skipToNextSteps(/* segmentsSkipped= */ 1);
-                }
-
-                float amplitude = stepSegment.getAmplitude();
-                if (amplitude == 0) {
-                    if (vibratorOffTimeout > now) {
-                        // Amplitude cannot be set to zero, so stop the vibrator.
-                        stopVibrating();
-                        mNextOffTime = now;
-                    }
-                } else {
-                    if (startTime >= mNextOffTime) {
-                        // Vibrator is OFF. Turn vibrator back on for the duration of another
-                        // cycle before setting the amplitude.
-                        long onDuration = getVibratorOnDuration(effect, segmentIndex);
-                        if (onDuration > 0) {
-                            mVibratorOnResult = startVibrating(onDuration);
-                            mNextOffTime = now + onDuration + CALLBACKS_EXTRA_TIMEOUT;
-                        }
-                    }
-                    changeAmplitude(amplitude);
-                }
-
-                // Use original startTime to avoid propagating latencies to the waveform.
-                long nextStartTime = startTime + segment.getDuration();
-                return nextSteps(nextStartTime, mNextOffTime, /* segmentsPlayed= */ 1);
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-            }
-        }
-
-        private long turnVibratorBackOn(long remainingDuration) {
-            long onDuration = getVibratorOnDuration(effect, segmentIndex);
-            if (onDuration <= 0) {
-                // Vibrator is supposed to go back off when this step starts, so just leave it off.
-                return vibratorOffTimeout;
-            }
-            onDuration += remainingDuration;
-            float expectedAmplitude = controller.getCurrentAmplitude();
-            mVibratorOnResult = startVibrating(onDuration);
-            if (mVibratorOnResult > 0) {
-                // Set the amplitude back to the value it was supposed to be playing at.
-                changeAmplitude(expectedAmplitude);
-            }
-            return SystemClock.uptimeMillis() + onDuration + CALLBACKS_EXTRA_TIMEOUT;
-        }
-
-        private long startVibrating(long duration) {
-            if (DEBUG) {
-                Slog.d(TAG, "Turning on vibrator " + controller.getVibratorInfo().getId() + " for "
-                        + duration + "ms");
-            }
-            return controller.on(duration, mVibration.id);
-        }
-
-        /**
-         * Get the duration the vibrator will be on for a waveform, starting at {@code startIndex}
-         * until the next time it's vibrating amplitude is zero or a different type of segment is
-         * found.
-         */
-        private long getVibratorOnDuration(VibrationEffect.Composed effect, int startIndex) {
-            List<VibrationEffectSegment> segments = effect.getSegments();
-            int segmentCount = segments.size();
-            int repeatIndex = effect.getRepeatIndex();
-            int i = startIndex;
-            long timing = 0;
-            while (i < segmentCount) {
-                VibrationEffectSegment segment = segments.get(i);
-                if (!(segment instanceof StepSegment)
-                        || ((StepSegment) segment).getAmplitude() == 0) {
-                    break;
-                }
-                timing += segment.getDuration();
-                i++;
-                if (i == segmentCount && repeatIndex >= 0) {
-                    i = repeatIndex;
-                    // prevent infinite loop
-                    repeatIndex = -1;
-                }
-                if (i == startIndex) {
-                    // The repeating waveform keeps the vibrator ON all the time. Use a minimum
-                    // of 1s duration to prevent short patterns from turning the vibrator ON too
-                    // frequently.
-                    return Math.max(timing, 1000);
-                }
-            }
-            if (i == segmentCount && effect.getRepeatIndex() < 0) {
-                // Vibration ending at non-zero amplitude, add extra timings to ramp down after
-                // vibration is complete.
-                timing += mVibrationSettings.getRampDownDuration();
-            }
-            return timing;
-        }
-    }
-
-    /**
-     * Map a {@link CombinedVibration} to the vibrators available on the device.
-     *
-     * <p>This contains the logic to find the capabilities required from {@link IVibratorManager} to
-     * play all of the effects in sync.
-     */
-    private final class DeviceEffectMap {
-        private final SparseArray<VibrationEffect.Composed> mVibratorEffects;
-        private final int[] mVibratorIds;
-        private final long mRequiredSyncCapabilities;
-
-        DeviceEffectMap(CombinedVibration.Mono mono) {
-            mVibratorEffects = new SparseArray<>(mVibrators.size());
-            mVibratorIds = new int[mVibrators.size()];
-            for (int i = 0; i < mVibrators.size(); i++) {
-                int vibratorId = mVibrators.keyAt(i);
-                VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo();
-                VibrationEffect effect = mDeviceEffectAdapter.apply(mono.getEffect(), vibratorInfo);
-                if (effect instanceof VibrationEffect.Composed) {
-                    mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect);
-                    mVibratorIds[i] = vibratorId;
-                }
-            }
-            mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects);
-        }
-
-        DeviceEffectMap(CombinedVibration.Stereo stereo) {
-            SparseArray<VibrationEffect> stereoEffects = stereo.getEffects();
-            mVibratorEffects = new SparseArray<>();
-            for (int i = 0; i < stereoEffects.size(); i++) {
-                int vibratorId = stereoEffects.keyAt(i);
-                if (mVibrators.contains(vibratorId)) {
-                    VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo();
-                    VibrationEffect effect = mDeviceEffectAdapter.apply(
-                            stereoEffects.valueAt(i), vibratorInfo);
-                    if (effect instanceof VibrationEffect.Composed) {
-                        mVibratorEffects.put(vibratorId, (VibrationEffect.Composed) effect);
-                    }
-                }
-            }
-            mVibratorIds = new int[mVibratorEffects.size()];
-            for (int i = 0; i < mVibratorEffects.size(); i++) {
-                mVibratorIds[i] = mVibratorEffects.keyAt(i);
-            }
-            mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects);
-        }
-
-        /**
-         * Return the number of vibrators mapped to play the {@link CombinedVibration} on this
-         * device.
-         */
-        public int size() {
-            return mVibratorIds.length;
-        }
-
-        /**
-         * Return all capabilities required to play the {@link CombinedVibration} in
-         * between calls to {@link IVibratorManager#prepareSynced} and
-         * {@link IVibratorManager#triggerSynced}.
-         */
-        public long getRequiredSyncCapabilities() {
-            return mRequiredSyncCapabilities;
-        }
-
-        /** Return all vibrator ids mapped to play the {@link CombinedVibration}. */
-        public int[] getVibratorIds() {
-            return mVibratorIds;
-        }
-
-        /** Return the id of the vibrator at given index. */
-        public int vibratorIdAt(int index) {
-            return mVibratorEffects.keyAt(index);
-        }
-
-        /** Return the {@link VibrationEffect} at given index. */
-        public VibrationEffect.Composed effectAt(int index) {
-            return mVibratorEffects.valueAt(index);
-        }
-
-        /**
-         * Return all capabilities required from the {@link IVibratorManager} to prepare and
-         * trigger all given effects in sync.
-         *
-         * @return {@link IVibratorManager#CAP_SYNC} together with all required
-         * IVibratorManager.CAP_PREPARE_* and IVibratorManager.CAP_MIXED_TRIGGER_* capabilities.
-         */
-        private long calculateRequiredSyncCapabilities(
-                SparseArray<VibrationEffect.Composed> effects) {
-            long prepareCap = 0;
-            for (int i = 0; i < effects.size(); i++) {
-                VibrationEffectSegment firstSegment = effects.valueAt(i).getSegments().get(0);
-                if (firstSegment instanceof StepSegment) {
-                    prepareCap |= IVibratorManager.CAP_PREPARE_ON;
-                } else if (firstSegment instanceof PrebakedSegment) {
-                    prepareCap |= IVibratorManager.CAP_PREPARE_PERFORM;
-                } else if (firstSegment instanceof PrimitiveSegment) {
-                    prepareCap |= IVibratorManager.CAP_PREPARE_COMPOSE;
-                }
-            }
-            int triggerCap = 0;
-            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_ON)) {
-                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_ON;
-            }
-            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_PERFORM)) {
-                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_PERFORM;
-            }
-            if (requireMixedTriggerCapability(prepareCap, IVibratorManager.CAP_PREPARE_COMPOSE)) {
-                triggerCap |= IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE;
-            }
-            return IVibratorManager.CAP_SYNC | prepareCap | triggerCap;
-        }
-
-        /**
-         * Return true if {@code prepareCapabilities} contains this {@code capability} mixed with
-         * different ones, requiring a mixed trigger capability from the vibrator manager for
-         * syncing all effects.
-         */
-        private boolean requireMixedTriggerCapability(long prepareCapabilities, long capability) {
-            return (prepareCapabilities & capability) != 0
-                    && (prepareCapabilities & ~capability) != 0;
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 63f3af3..01f9d0b9 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -42,6 +42,7 @@
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.ShellCallback;
@@ -60,6 +61,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 
@@ -88,6 +90,9 @@
             VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
                     | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
 
+    /** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */
+    private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000;
+
     /** Lifecycle responsible for initializing this class at the right system server phases. */
     public static class Lifecycle extends SystemService {
         private VibratorManagerService mService;
@@ -201,8 +206,7 @@
         mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class)
                 .getSystemUiServiceComponent().getPackageName();
 
-        mBatteryStatsService = IBatteryStats.Stub.asInterface(ServiceManager.getService(
-                BatteryStats.SERVICE_NAME));
+        mBatteryStatsService = injector.getBatteryStatsService();
 
         mAppOps = mContext.getSystemService(AppOpsManager.class);
 
@@ -634,7 +638,7 @@
             }
 
             VibrationThread vibThread = new VibrationThread(vib, mVibrationSettings,
-                    mDeviceVibrationEffectAdapter, mVibrators, mWakeLock, mBatteryStatsService,
+                    mDeviceVibrationEffectAdapter, mVibrators, mWakeLock,
                     mVibrationThreadCallbacks);
 
             if (mCurrentVibration == null) {
@@ -1105,6 +1109,11 @@
             return new Handler(looper);
         }
 
+        IBatteryStats getBatteryStatsService() {
+            return IBatteryStats.Stub.asInterface(ServiceManager.getService(
+                    BatteryStats.SERVICE_NAME));
+        }
+
         VibratorController createVibratorController(int vibratorId,
                 VibratorController.OnVibrationCompleteListener listener) {
             return new VibratorController(vibratorId, listener);
@@ -1141,6 +1150,36 @@
         }
 
         @Override
+        public void noteVibratorOn(int uid, long duration) {
+            try {
+                if (duration <= 0) {
+                    return;
+                }
+                if (duration == Long.MAX_VALUE) {
+                    // Repeating duration has started. Report a fixed duration here, noteVibratorOff
+                    // should be called when this is cancelled.
+                    duration = BATTERY_STATS_REPEATING_VIBRATION_DURATION;
+                }
+                mBatteryStatsService.noteVibratorOn(uid, duration);
+                FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
+                        uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__ON,
+                        duration);
+            } catch (RemoteException e) {
+            }
+        }
+
+        @Override
+        public void noteVibratorOff(int uid) {
+            try {
+                mBatteryStatsService.noteVibratorOff(uid);
+                FrameworkStatsLog.write_non_chained(FrameworkStatsLog.VIBRATOR_STATE_CHANGED,
+                        uid, null, FrameworkStatsLog.VIBRATOR_STATE_CHANGED__STATE__OFF,
+                        /* duration= */ 0);
+            } catch (RemoteException e) {
+            }
+        }
+
+        @Override
         public void onVibrationCompleted(long vibrationId, Vibration.Status status) {
             if (DEBUG) {
                 Slog.d(TAG, "Vibration " + vibrationId + " finished with status " + status);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index ffa1a60..98c74f8 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5149,10 +5149,13 @@
             // Ensure that we do not trigger entering PiP an activity on the root pinned task
             return false;
         }
+        final boolean isTransient = opts != null && opts.getTransientLaunch();
         final Task targetRootTask = toFrontTask != null
                 ? toFrontTask.getRootTask() : toFrontActivity.getRootTask();
-        if (targetRootTask != null && targetRootTask.isActivityTypeAssistant()) {
-            // Ensure the task/activity being brought forward is not the assistant
+        if (targetRootTask != null && (targetRootTask.isActivityTypeAssistant() || isTransient)) {
+            // Ensure the task/activity being brought forward is not the assistant and is not
+            // transient. In the case of transient-launch, we want to wait until the end of the
+            // transition and only allow switch if the transient launch was committed.
             return false;
         }
         return true;
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index a9add59..6a23eb5 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -310,7 +310,7 @@
         }
         final ActivityRecord activity = result.first;
         final WindowState mainWindow = result.second;
-        final Rect contentInsets = getSystemBarInsets(task.getBounds(),
+        final Rect contentInsets = getSystemBarInsets(mainWindow.getFrame(),
                 mainWindow.getInsetsStateWithVisibilityOverride());
         final Rect letterboxInsets = activity.getLetterboxInsets();
         InsetUtils.addInsets(contentInsets, letterboxInsets);
@@ -575,7 +575,7 @@
         final LayoutParams attrs = mainWindow.getAttrs();
         final Rect taskBounds = task.getBounds();
         final InsetsState insetsState = mainWindow.getInsetsStateWithVisibilityOverride();
-        final Rect systemBarInsets = getSystemBarInsets(taskBounds, insetsState);
+        final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState);
         final SystemBarBackgroundPainter decorPainter = new SystemBarBackgroundPainter(attrs.flags,
                 attrs.privateFlags, attrs.insetsFlags.appearance, task.getTaskDescription(),
                 mHighResTaskSnapshotScale, insetsState);
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 0b965c3..3a3103e 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -459,9 +459,19 @@
                 // activity in a bad state.
                 if (!visibleAtTransitionEnd && !ar.isVisibleRequested()) {
                     boolean commitVisibility = true;
-                    if (ar.getDeferHidingClient() && ar.getTask() != null) {
+                    if (ar.isVisible() && ar.getTask() != null) {
                         if (ar.pictureInPictureArgs != null
                                 && ar.pictureInPictureArgs.isAutoEnterEnabled()) {
+                            if (mTransientLaunches != null) {
+                                for (int j = 0; j < mTransientLaunches.size(); ++j) {
+                                    if (mTransientLaunches.valueAt(j).isVisibleRequested()) {
+                                        // force enable pip-on-task-switch now that we've committed
+                                        // to actually launching to the transient activity.
+                                        ar.supportsEnterPipOnTaskSwitch = true;
+                                        break;
+                                    }
+                                }
+                            }
                             mController.mAtm.enterPictureInPictureMode(ar, ar.pictureInPictureArgs);
                             // Avoid commit visibility to false here, or else we will get a sudden
                             // "flash" / surface going invisible for a split second.
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index c267cba..c13ae95 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -30,6 +30,7 @@
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.IApplicationThread;
+import android.app.WindowConfiguration;
 import android.os.IBinder;
 import android.os.IRemoteCallback;
 import android.os.RemoteException;
@@ -296,6 +297,17 @@
         return ci.mVisible;
     }
 
+    @WindowConfiguration.WindowingMode
+    int getWindowingModeAtStart(@NonNull WindowContainer wc) {
+        if (mCollectingTransition == null) return wc.getWindowingMode();
+        final Transition.ChangeInfo ci = mCollectingTransition.mChanges.get(wc);
+        if (ci == null) {
+            // not part of transition, so use current state.
+            return wc.getWindowingMode();
+        }
+        return ci.mWindowingMode;
+    }
+
     @WindowManager.TransitionType
     int getCollectingTransitionType() {
         return mCollectingTransition != null ? mCollectingTransition.mType : TRANSIT_NONE;
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index ccaa03a..a2e8813 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 
@@ -645,8 +646,10 @@
         final ActivityRecord r = asActivityRecord();
         if (r != null) {
             final Task rootTask = r.getRootTask();
-            // Don't transform the activity in PiP because the PiP task organizer will handle it.
-            if (rootTask != null && rootTask.inPinnedWindowingMode()) {
+            // Don't transform the activity exiting PiP because the PiP task organizer will handle
+            // it.
+            if (rootTask != null && mTransitionController.getWindowingModeAtStart(rootTask)
+                    == WINDOWING_MODE_PINNED) {
                 return;
             }
         }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 091d879..33e97aa 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -31,20 +31,6 @@
 import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE;
 import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_USER;
 import static android.app.admin.DevicePolicyManager.ACTION_SYSTEM_UPDATE_POLICY_CHANGED;
-import static android.app.admin.DevicePolicyManager.CODE_ACCOUNTS_NOT_EMPTY;
-import static android.app.admin.DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE;
-import static android.app.admin.DevicePolicyManager.CODE_DEVICE_ADMIN_NOT_SUPPORTED;
-import static android.app.admin.DevicePolicyManager.CODE_HAS_DEVICE_OWNER;
-import static android.app.admin.DevicePolicyManager.CODE_HAS_PAIRED;
-import static android.app.admin.DevicePolicyManager.CODE_MANAGED_USERS_NOT_SUPPORTED;
-import static android.app.admin.DevicePolicyManager.CODE_NONSYSTEM_USER_EXISTS;
-import static android.app.admin.DevicePolicyManager.CODE_NOT_SYSTEM_USER;
-import static android.app.admin.DevicePolicyManager.CODE_OK;
-import static android.app.admin.DevicePolicyManager.CODE_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS;
-import static android.app.admin.DevicePolicyManager.CODE_SYSTEM_USER;
-import static android.app.admin.DevicePolicyManager.CODE_USER_HAS_PROFILE_OWNER;
-import static android.app.admin.DevicePolicyManager.CODE_USER_NOT_RUNNING;
-import static android.app.admin.DevicePolicyManager.CODE_USER_SETUP_COMPLETED;
 import static android.app.admin.DevicePolicyManager.DELEGATION_APP_RESTRICTIONS;
 import static android.app.admin.DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL;
 import static android.app.admin.DevicePolicyManager.DELEGATION_CERT_INSTALL;
@@ -101,6 +87,20 @@
 import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_SET_NO_ERROR;
 import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
 import static android.app.admin.DevicePolicyManager.STATE_USER_UNMANAGED;
+import static android.app.admin.DevicePolicyManager.STATUS_ACCOUNTS_NOT_EMPTY;
+import static android.app.admin.DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE;
+import static android.app.admin.DevicePolicyManager.STATUS_DEVICE_ADMIN_NOT_SUPPORTED;
+import static android.app.admin.DevicePolicyManager.STATUS_HAS_DEVICE_OWNER;
+import static android.app.admin.DevicePolicyManager.STATUS_HAS_PAIRED;
+import static android.app.admin.DevicePolicyManager.STATUS_MANAGED_USERS_NOT_SUPPORTED;
+import static android.app.admin.DevicePolicyManager.STATUS_NONSYSTEM_USER_EXISTS;
+import static android.app.admin.DevicePolicyManager.STATUS_NOT_SYSTEM_USER;
+import static android.app.admin.DevicePolicyManager.STATUS_OK;
+import static android.app.admin.DevicePolicyManager.STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS;
+import static android.app.admin.DevicePolicyManager.STATUS_SYSTEM_USER;
+import static android.app.admin.DevicePolicyManager.STATUS_USER_HAS_PROFILE_OWNER;
+import static android.app.admin.DevicePolicyManager.STATUS_USER_NOT_RUNNING;
+import static android.app.admin.DevicePolicyManager.STATUS_USER_SETUP_COMPLETED;
 import static android.app.admin.DevicePolicyManager.WIPE_EUICC;
 import static android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE;
 import static android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA;
@@ -9658,7 +9658,7 @@
         final int code = checkDeviceOwnerProvisioningPreConditionLocked(owner,
                 /* deviceOwnerUserId= */ deviceOwnerUserId, /* callingUserId*/ caller.getUserId(),
                 isAdb(caller), hasIncompatibleAccountsOrNonAdb);
-        if (code != CODE_OK) {
+        if (code != STATUS_OK) {
             throw new IllegalStateException(
                     computeProvisioningErrorString(code, deviceOwnerUserId));
         }
@@ -9666,25 +9666,25 @@
 
     private static String computeProvisioningErrorString(int code, @UserIdInt int userId) {
         switch (code) {
-            case CODE_OK:
+            case STATUS_OK:
                 return "OK";
-            case CODE_HAS_DEVICE_OWNER:
+            case STATUS_HAS_DEVICE_OWNER:
                 return "Trying to set the device owner, but device owner is already set.";
-            case CODE_USER_HAS_PROFILE_OWNER:
+            case STATUS_USER_HAS_PROFILE_OWNER:
                 return "Trying to set the device owner, but the user already has a profile owner.";
-            case CODE_USER_NOT_RUNNING:
+            case STATUS_USER_NOT_RUNNING:
                 return "User " + userId + " not running.";
-            case CODE_NOT_SYSTEM_USER:
+            case STATUS_NOT_SYSTEM_USER:
                 return "User " + userId + " is not system user.";
-            case CODE_USER_SETUP_COMPLETED:
+            case STATUS_USER_SETUP_COMPLETED:
                 return  "Cannot set the device owner if the device is already set-up.";
-            case CODE_NONSYSTEM_USER_EXISTS:
+            case STATUS_NONSYSTEM_USER_EXISTS:
                 return "Not allowed to set the device owner because there are already several"
                         + " users on the device.";
-            case CODE_ACCOUNTS_NOT_EMPTY:
+            case STATUS_ACCOUNTS_NOT_EMPTY:
                 return "Not allowed to set the device owner because there are already some accounts"
                         + " on the device.";
-            case CODE_HAS_PAIRED:
+            case STATUS_HAS_PAIRED:
                 return "Not allowed to set the device owner because this device has already "
                         + "paired.";
             default:
@@ -14159,30 +14159,30 @@
             mInjector.binderRestoreCallingIdentity(ident);
         }
 
-        return checkProvisioningPreConditionSkipPermission(action, packageName) == CODE_OK;
+        return checkProvisioningPreconditionSkipPermission(action, packageName) == STATUS_OK;
     }
 
     @Override
-    public int checkProvisioningPreCondition(String action, String packageName) {
+    public int checkProvisioningPrecondition(String action, String packageName) {
         Objects.requireNonNull(packageName, "packageName is null");
 
         Preconditions.checkCallAuthorization(
                 hasCallingOrSelfPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS));
 
-        return checkProvisioningPreConditionSkipPermission(action, packageName);
+        return checkProvisioningPreconditionSkipPermission(action, packageName);
     }
 
-    private int checkProvisioningPreConditionSkipPermission(String action,
+    private int checkProvisioningPreconditionSkipPermission(String action,
             String packageName) {
         if (!mHasFeature) {
             logMissingFeatureAction("Cannot check provisioning for action " + action);
-            return CODE_DEVICE_ADMIN_NOT_SUPPORTED;
+            return STATUS_DEVICE_ADMIN_NOT_SUPPORTED;
         }
         if (!isProvisioningAllowed()) {
-            return CODE_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS;
+            return STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS;
         }
         final int code = checkProvisioningPreConditionSkipPermissionNoLog(action, packageName);
-        if (code != CODE_OK) {
+        if (code != STATUS_OK) {
             Slogf.d(LOG_TAG, "checkProvisioningPreCondition(" + action + ", " + packageName
                     + ") failed: "
                     + computeProvisioningErrorString(code, mInjector.userHandleGetCallingUserId()));
@@ -14230,27 +14230,27 @@
             @UserIdInt int deviceOwnerUserId, @UserIdInt int callingUserId, boolean isAdb,
             boolean hasIncompatibleAccountsOrNonAdb) {
         if (mOwners.hasDeviceOwner()) {
-            return CODE_HAS_DEVICE_OWNER;
+            return STATUS_HAS_DEVICE_OWNER;
         }
         if (mOwners.hasProfileOwner(deviceOwnerUserId)) {
-            return CODE_USER_HAS_PROFILE_OWNER;
+            return STATUS_USER_HAS_PROFILE_OWNER;
         }
 
         boolean isHeadlessSystemUserMode = mInjector.userManagerIsHeadlessSystemUserMode();
         // System user is always running in headless system user mode.
         if (!isHeadlessSystemUserMode
                 && !mUserManager.isUserRunning(new UserHandle(deviceOwnerUserId))) {
-            return CODE_USER_NOT_RUNNING;
+            return STATUS_USER_NOT_RUNNING;
         }
         if (mIsWatch && hasPaired(UserHandle.USER_SYSTEM)) {
-            return CODE_HAS_PAIRED;
+            return STATUS_HAS_PAIRED;
         }
 
         if (isHeadlessSystemUserMode) {
             if (deviceOwnerUserId != UserHandle.USER_SYSTEM) {
                 Slogf.e(LOG_TAG, "In headless system user mode, "
                         + "device owner can only be set on headless system user.");
-                return CODE_NOT_SYSTEM_USER;
+                return STATUS_NOT_SYSTEM_USER;
             }
         }
 
@@ -14265,7 +14265,7 @@
 
                 int maxNumberOfExistingUsers = isHeadlessSystemUserMode ? 2 : 1;
                 if (mUserManager.getUserCount() > maxNumberOfExistingUsers) {
-                    return CODE_NONSYSTEM_USER_EXISTS;
+                    return STATUS_NONSYSTEM_USER_EXISTS;
                 }
 
                 int currentForegroundUser = getCurrentForegroundUserId();
@@ -14274,23 +14274,23 @@
                         && currentForegroundUser == UserHandle.USER_SYSTEM) {
                     Slogf.wtf(LOG_TAG, "In headless system user mode, "
                             + "current user cannot be system user when setting device owner");
-                    return CODE_SYSTEM_USER;
+                    return STATUS_SYSTEM_USER;
                 }
                 if (hasIncompatibleAccountsOrNonAdb) {
-                    return CODE_ACCOUNTS_NOT_EMPTY;
+                    return STATUS_ACCOUNTS_NOT_EMPTY;
                 }
             }
-            return CODE_OK;
+            return STATUS_OK;
         } else {
             // DO has to be user 0
             if (deviceOwnerUserId != UserHandle.USER_SYSTEM) {
-                return CODE_NOT_SYSTEM_USER;
+                return STATUS_NOT_SYSTEM_USER;
             }
             // Only provision DO before setup wizard completes
             if (hasUserSetupCompleted(UserHandle.USER_SYSTEM)) {
-                return CODE_USER_SETUP_COMPLETED;
+                return STATUS_USER_SETUP_COMPLETED;
             }
-            return CODE_OK;
+            return STATUS_OK;
         }
     }
 
@@ -14311,11 +14311,11 @@
     private int checkManagedProfileProvisioningPreCondition(String packageName,
             @UserIdInt int callingUserId) {
         if (!hasFeatureManagedUsers()) {
-            return CODE_MANAGED_USERS_NOT_SUPPORTED;
+            return STATUS_MANAGED_USERS_NOT_SUPPORTED;
         }
         if (getProfileOwnerAsUser(callingUserId) != null) {
             // Managed user cannot have a managed profile.
-            return CODE_USER_HAS_PROFILE_OWNER;
+            return STATUS_USER_HAS_PROFILE_OWNER;
         }
 
         final long ident = mInjector.binderClearCallingIdentity();
@@ -14334,7 +14334,7 @@
                         callingUserId);
                 // The check is called from inside a managed profile. A managed profile cannot
                 // be provisioned from within another managed profile.
-                return CODE_CANNOT_ADD_MANAGED_PROFILE;
+                return STATUS_CANNOT_ADD_MANAGED_PROFILE;
             }
 
             // If there's a device owner, the restriction on adding a managed profile must be set.
@@ -14346,19 +14346,19 @@
             if (addingProfileRestricted) {
                 Slogf.i(LOG_TAG, "Adding a profile is restricted: User %s Has device owner? %b",
                         callingUserHandle, hasDeviceOwner);
-                return CODE_CANNOT_ADD_MANAGED_PROFILE;
+                return STATUS_CANNOT_ADD_MANAGED_PROFILE;
             }
 
             // Bail out if we are trying to provision a work profile but one already exists.
             if (!mUserManager.canAddMoreManagedProfiles(
                     callingUserId, /* allowedToRemoveOne= */ false)) {
                 Slogf.i(LOG_TAG, "A work profile already exists.");
-                return CODE_CANNOT_ADD_MANAGED_PROFILE;
+                return STATUS_CANNOT_ADD_MANAGED_PROFILE;
             }
         } finally {
             mInjector.binderRestoreCallingIdentity(ident);
         }
-        return CODE_OK;
+        return STATUS_OK;
     }
 
     private void checkIsDeviceOwner(CallerIdentity caller) {
@@ -17697,9 +17697,9 @@
         UserInfo userInfo = null;
         final long identity = Binder.clearCallingIdentity();
         try {
-            final int result = checkProvisioningPreConditionSkipPermission(
+            final int result = checkProvisioningPreconditionSkipPermission(
                     ACTION_PROVISION_MANAGED_PROFILE, admin.getPackageName());
-            if (result != CODE_OK) {
+            if (result != STATUS_OK) {
                 throw new ServiceSpecificException(
                         ERROR_PRE_CONDITION_FAILED,
                         "Provisioning preconditions failed with result: " + result);
@@ -18076,9 +18076,9 @@
 
         final long identity = Binder.clearCallingIdentity();
         try {
-            int result = checkProvisioningPreConditionSkipPermission(
+            int result = checkProvisioningPreconditionSkipPermission(
                     ACTION_PROVISION_MANAGED_DEVICE, deviceAdmin.getPackageName());
-            if (result != CODE_OK) {
+            if (result != STATUS_OK) {
                 throw new ServiceSpecificException(
                         ERROR_PRE_CONDITION_FAILED,
                         "Provisioning preconditions failed with result: " + result);
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 cd2d0fc..83ccabf 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
@@ -238,6 +238,7 @@
         AndroidPackage::isVendor,
         AndroidPackage::isVisibleToInstantApps,
         AndroidPackage::isVmSafeMode,
+        AndroidPackage::isLeavingSharedUid,
         AndroidPackage::isResetEnabledSettingsOnAppDataCleared,
         AndroidPackage::getMaxAspectRatio,
         AndroidPackage::getMinAspectRatio,
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
index 890a549..4e4854c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
@@ -333,6 +333,10 @@
 
     @Test
     public void testGetLastLocation_Bypass() {
+        mInjector.getSettingsHelper().setIgnoreSettingsAllowlist(
+                new PackageTagsList.Builder().add(
+                        IDENTITY.getPackageName()).build());
+
         assertThat(mManager.getLastLocation(new LastLocationRequest.Builder().build(), IDENTITY,
                 PERMISSION_FINE)).isNull();
         assertThat(mManager.getLastLocation(
@@ -381,6 +385,14 @@
                 new LastLocationRequest.Builder().setLocationSettingsIgnored(true).build(),
                 IDENTITY, PERMISSION_FINE)).isEqualTo(
                 loc);
+
+        mInjector.getSettingsHelper().setIgnoreSettingsAllowlist(
+                new PackageTagsList.Builder().build());
+        mProvider.setProviderAllowed(false);
+
+        assertThat(mManager.getLastLocation(
+                new LastLocationRequest.Builder().setLocationSettingsIgnored(true).build(),
+                IDENTITY, PERMISSION_FINE)).isNull();
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
index 6751b80..41d46f2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
@@ -73,6 +73,7 @@
                 .startMocking();
         when(mIrs.getContext()).thenReturn(mContext);
         when(mIrs.getCompleteEconomicPolicyLocked()).thenReturn(mEconomicPolicy);
+        when(mIrs.getLock()).thenReturn(mIrs);
         when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mock(AlarmManager.class));
         mScribe = new MockScribe(mIrs);
     }
@@ -89,75 +90,75 @@
         Agent agent = new Agent(mIrs, mScribe);
         Ledger ledger = new Ledger();
 
-        doReturn(1_000_000L).when(mIrs).getMaxCirculationLocked();
+        doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
         doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance();
 
-        Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5);
+        Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(5, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 995);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 995, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1000, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, -500);
+        transaction = new Ledger.Transaction(0, 0, 0, null, -500, 250);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(500, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 999_500L);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 999_500L, 500);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1_000_000L, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, -1_000_001L);
+        transaction = new Ledger.Transaction(0, 0, 0, null, -1_000_001L, 1000);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(-1, ledger.getCurrentBalance());
     }
 
     @Test
-    public void testRecordTransaction_MaxCirculation() {
+    public void testRecordTransaction_MaxConsumptionLimit() {
         Agent agent = new Agent(mIrs, mScribe);
         Ledger ledger = new Ledger();
 
-        doReturn(1000L).when(mIrs).getMaxCirculationLocked();
-        doReturn(1000L).when(mEconomicPolicy).getMaxSatiatedBalance();
+        doReturn(1000L).when(mIrs).getConsumptionLimitLocked();
+        doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance();
 
-        Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5);
+        Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(5, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 995);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 995, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1000, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, -500);
+        transaction = new Ledger.Transaction(0, 0, 0, null, -500, 250);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(500, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 2000);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 2000, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
-        assertEquals(1000, ledger.getCurrentBalance());
+        assertEquals(2500, ledger.getCurrentBalance());
 
-        // MaxCirculation can change as the battery level changes. Any already allocated ARCSs
-        // shouldn't be removed by recordTransaction().
-        doReturn(900L).when(mIrs).getMaxCirculationLocked();
+        // ConsumptionLimit can change as the battery level changes. Ledger balances shouldn't be
+        // affected.
+        doReturn(900L).when(mIrs).getConsumptionLimitLocked();
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 100);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 100, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
-        assertEquals(1000, ledger.getCurrentBalance());
+        assertEquals(2600, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, -50);
+        transaction = new Ledger.Transaction(0, 0, 0, null, -50, 50);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
-        assertEquals(950, ledger.getCurrentBalance());
+        assertEquals(2550, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, -200);
+        transaction = new Ledger.Transaction(0, 0, 0, null, -200, 100);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
-        assertEquals(750, ledger.getCurrentBalance());
+        assertEquals(2350, ledger.getCurrentBalance());
 
-        doReturn(800L).when(mIrs).getMaxCirculationLocked();
+        doReturn(800L).when(mIrs).getConsumptionLimitLocked();
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 100);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 100, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
-        assertEquals(800, ledger.getCurrentBalance());
+        assertEquals(2450, ledger.getCurrentBalance());
     }
 
     @Test
@@ -165,33 +166,33 @@
         Agent agent = new Agent(mIrs, mScribe);
         Ledger ledger = new Ledger();
 
-        doReturn(1_000_000L).when(mIrs).getMaxCirculationLocked();
+        doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
         doReturn(1000L).when(mEconomicPolicy).getMaxSatiatedBalance();
 
-        Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5);
+        Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(5, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 995);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 995, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1000, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, -500);
+        transaction = new Ledger.Transaction(0, 0, 0, null, -500, 250);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(500, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 999_500L);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 999_500L, 1000);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1_000, ledger.getCurrentBalance());
 
         // Shouldn't change in normal operation, but adding test case in case it does.
         doReturn(900L).when(mEconomicPolicy).getMaxSatiatedBalance();
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, 500);
+        transaction = new Ledger.Transaction(0, 0, 0, null, 500, 0);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1_000, ledger.getCurrentBalance());
 
-        transaction = new Ledger.Transaction(0, 0, 0, null, -1001);
+        transaction = new Ledger.Transaction(0, 0, 0, null, -1001, 500);
         agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(-1, ledger.getCurrentBalance());
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
index b72fc23..ab29e59 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
@@ -107,20 +107,26 @@
     @Test
     public void testWriteHighLevelStateToDisk() {
         long lastReclamationTime = System.currentTimeMillis();
-        long narcsInCirculation = 2000L;
+        long remainingConsumableNarcs = 2000L;
+        long consumptionLimit = 500_000L;
 
         Ledger ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
-        ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, 2000));
+        ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, 2000, 0));
         // Negative ledger balance shouldn't affect the total circulation value.
         ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID + 1, TEST_PACKAGE);
-        ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, -5000));
+        ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, -5000, 3000));
         mScribeUnderTest.setLastReclamationTimeLocked(lastReclamationTime);
+        mScribeUnderTest.setConsumptionLimitLocked(consumptionLimit);
+        mScribeUnderTest.adjustRemainingConsumableNarcsLocked(
+                remainingConsumableNarcs - consumptionLimit);
         mScribeUnderTest.writeImmediatelyForTesting();
 
         mScribeUnderTest.loadFromDiskLocked();
 
         assertEquals(lastReclamationTime, mScribeUnderTest.getLastReclamationTimeLocked());
-        assertEquals(narcsInCirculation, mScribeUnderTest.getNarcsInCirculationLocked());
+        assertEquals(remainingConsumableNarcs,
+                mScribeUnderTest.getRemainingConsumableNarcsLocked());
+        assertEquals(consumptionLimit, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
     }
 
     @Test
@@ -135,9 +141,9 @@
     @Test
     public void testWritingPopulatedLedgerToDisk() {
         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
-        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
-        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
-        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
+        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 0));
+        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, -1));
+        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 12));
         mScribeUnderTest.writeImmediatelyForTesting();
 
         mScribeUnderTest.loadFromDiskLocked();
@@ -156,11 +162,11 @@
                 addInstalledPackage(userId, pkgName);
                 final Ledger ledger = mScribeUnderTest.getLedgerLocked(userId, pkgName);
                 ledger.recordTransaction(new Ledger.Transaction(
-                        0, 1000L * u + l, 1, null, 51L * u + l));
+                        0, 1000L * u + l, 1, null, -51L * u + l, 50));
                 ledger.recordTransaction(new Ledger.Transaction(
-                        1500L * u + l, 2000L * u + l, 2 * u + l, "green" + u + l, 52L * u + l));
+                        1500L * u + l, 2000L * u + l, 2 * u + l, "green" + u + l, 52L * u + l, 0));
                 ledger.recordTransaction(new Ledger.Transaction(
-                        2500L * u + l, 3000L * u + l, 3 * u + l, "blue" + u + l, 3L * u + l));
+                        2500L * u + l, 3000L * u + l, 3 * u + l, "blue" + u + l, 3L * u + l, 0));
                 ledgers.add(userId, pkgName, ledger);
             }
         }
@@ -174,9 +180,9 @@
     @Test
     public void testDiscardLedgerFromDisk() {
         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
-        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
-        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
-        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
+        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 1));
+        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 0));
+        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 1));
         mScribeUnderTest.writeImmediatelyForTesting();
 
         mScribeUnderTest.loadFromDiskLocked();
@@ -195,9 +201,9 @@
     public void testLoadingMissingPackageFromDisk() {
         final String pkgName = TEST_PACKAGE + ".uninstalled";
         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, pkgName);
-        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
-        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
-        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
+        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 1));
+        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 2));
+        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 3));
         mScribeUnderTest.writeImmediatelyForTesting();
 
         // Package isn't installed, so make sure it's not saved to memory after loading.
@@ -209,9 +215,9 @@
     public void testLoadingMissingUserFromDisk() {
         final int userId = TEST_USER_ID + 1;
         final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(userId, TEST_PACKAGE);
-        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
-        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
-        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
+        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51, 0));
+        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52, 1));
+        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3, 3));
         mScribeUnderTest.writeImmediatelyForTesting();
 
         // User doesn't show up with any packages, so make sure nothing is saved after loading.
@@ -219,6 +225,37 @@
         assertLedgersEqual(new Ledger(), mScribeUnderTest.getLedgerLocked(userId, TEST_PACKAGE));
     }
 
+    @Test
+    public void testChangingConsumable() {
+        assertEquals(0, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
+        assertEquals(0, mScribeUnderTest.getRemainingConsumableNarcsLocked());
+
+        // Limit increased, so remaining value should be adjusted as well
+        mScribeUnderTest.setConsumptionLimitLocked(1000);
+        assertEquals(1000, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
+        assertEquals(1000, mScribeUnderTest.getRemainingConsumableNarcsLocked());
+
+        // Limit decreased below remaining, so remaining value should be adjusted as well
+        mScribeUnderTest.setConsumptionLimitLocked(500);
+        assertEquals(500, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
+        assertEquals(500, mScribeUnderTest.getRemainingConsumableNarcsLocked());
+
+        mScribeUnderTest.adjustRemainingConsumableNarcsLocked(-100);
+        assertEquals(500, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
+        assertEquals(400, mScribeUnderTest.getRemainingConsumableNarcsLocked());
+
+        // Limit increased, so remaining value should be adjusted by the difference as well
+        mScribeUnderTest.setConsumptionLimitLocked(1000);
+        assertEquals(1000, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
+        assertEquals(900, mScribeUnderTest.getRemainingConsumableNarcsLocked());
+
+
+        // Limit decreased, but above remaining, so remaining value should left alone
+        mScribeUnderTest.setConsumptionLimitLocked(950);
+        assertEquals(950, mScribeUnderTest.getSatiatedConsumptionLimitLocked());
+        assertEquals(900, mScribeUnderTest.getRemainingConsumableNarcsLocked());
+    }
+
     private void assertLedgersEqual(Ledger expected, Ledger actual) {
         if (expected == null) {
             assertNull(actual);
@@ -245,6 +282,7 @@
         assertEquals(expected.eventId, actual.eventId);
         assertEquals(expected.tag, actual.tag);
         assertEquals(expected.delta, actual.delta);
+        assertEquals(expected.ctp, actual.ctp);
     }
 
     private void addInstalledPackage(int userId, String pkgName) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index a9b7cfb..3ce2ed8 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -52,7 +52,6 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.server.accessibility.AccessibilityManagerService;
 import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.test.MessageCapturingHandler;
 import com.android.server.wm.WindowManagerInternal;
@@ -94,7 +93,6 @@
     final FullScreenMagnificationController.ControllerContext mMockControllerCtx =
             mock(FullScreenMagnificationController.ControllerContext.class);
     final Context mMockContext = mock(Context.class);
-    final AccessibilityManagerService mMockAms = mock(AccessibilityManagerService.class);
     final AccessibilityTraceManager mMockTraceManager = mock(AccessibilityTraceManager.class);
     final WindowManagerInternal mMockWindowManager = mock(WindowManagerInternal.class);
     private final MagnificationAnimationCallback mAnimationCallback = mock(
@@ -121,12 +119,10 @@
         // Pretending ID of the Thread associated with looper as main thread ID in controller
         when(mMockContext.getMainLooper()).thenReturn(looper);
         when(mMockControllerCtx.getContext()).thenReturn(mMockContext);
-        when(mMockControllerCtx.getAms()).thenReturn(mMockAms);
         when(mMockControllerCtx.getTraceManager()).thenReturn(mMockTraceManager);
         when(mMockControllerCtx.getWindowManager()).thenReturn(mMockWindowManager);
         when(mMockControllerCtx.getHandler()).thenReturn(mMessageCapturingHandler);
         when(mMockControllerCtx.getAnimationDuration()).thenReturn(1000L);
-        when(mMockAms.getTraceManager()).thenReturn(mMockTraceManager);
         initMockWindowManager();
 
         mFullScreenMagnificationController = new FullScreenMagnificationController(
@@ -357,8 +353,8 @@
         assertEquals(newCenter.x, mFullScreenMagnificationController.getCenterX(displayId), 0.5);
         assertEquals(newCenter.y, mFullScreenMagnificationController.getCenterY(displayId), 0.5);
         assertThat(getCurrentMagnificationSpec(displayId), closeTo(endSpec));
-        verify(mMockAms).notifyMagnificationChanged(eq(displayId), eq(INITIAL_MAGNIFICATION_REGION),
-                mConfigCaptor.capture());
+        verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId),
+                eq(INITIAL_MAGNIFICATION_REGION), mConfigCaptor.capture());
         assertConfigEquals(config, mConfigCaptor.getValue());
         verify(mMockValueAnimator).start();
         verify(mRequestObserver).onRequestMagnificationSpec(displayId, SERVICE_ID_1);
@@ -501,7 +497,7 @@
         mMessageCapturingHandler.sendAllMessages();
         MagnificationConfig config = buildConfig(1.0f, OTHER_MAGNIFICATION_BOUNDS.centerX(),
                 OTHER_MAGNIFICATION_BOUNDS.centerY());
-        verify(mMockAms).notifyMagnificationChanged(eq(displayId), eq(OTHER_REGION),
+        verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId), eq(OTHER_REGION),
                 mConfigCaptor.capture());
         assertConfigEquals(config, mConfigCaptor.getValue());
     }
@@ -655,9 +651,9 @@
         register(displayId);
         zoomIn2xToMiddle(displayId);
         mMessageCapturingHandler.sendAllMessages();
-        reset(mMockAms);
+        reset(mRequestObserver);
         assertTrue(mFullScreenMagnificationController.resetIfNeeded(displayId, false));
-        verify(mMockAms).notifyMagnificationChanged(eq(displayId),
+        verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId),
                 eq(INITIAL_MAGNIFICATION_REGION), any(MagnificationConfig.class));
         assertFalse(mFullScreenMagnificationController.isMagnifying(displayId));
         assertFalse(mFullScreenMagnificationController.resetIfNeeded(displayId, false));
@@ -676,8 +672,8 @@
         assertFalse(mFullScreenMagnificationController.reset(displayId, mAnimationCallback));
         mMessageCapturingHandler.sendAllMessages();
 
-        verify(mMockAms, never()).notifyMagnificationChanged(eq(displayId), any(Region.class),
-                any(MagnificationConfig.class));
+        verify(mRequestObserver, never()).onFullScreenMagnificationChanged(eq(displayId),
+                any(Region.class), any(MagnificationConfig.class));
         verify(mAnimationCallback).onResult(true);
     }
 
@@ -1072,8 +1068,8 @@
         when(mMockValueAnimator.getAnimatedFraction()).thenReturn(0.0f);
         mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
         verify(mMockWindowManager).setMagnificationSpec(eq(displayId), eq(startSpec));
-        verify(mMockAms).notifyMagnificationChanged(eq(displayId), eq(INITIAL_MAGNIFICATION_REGION),
-                mConfigCaptor.capture());
+        verify(mRequestObserver).onFullScreenMagnificationChanged(eq(displayId),
+                eq(INITIAL_MAGNIFICATION_REGION), mConfigCaptor.capture());
         assertConfigEquals(config, mConfigCaptor.getValue());
         Mockito.reset(mMockWindowManager);
 
@@ -1097,7 +1093,7 @@
 
         // Animation should have been restarted
         verify(mMockValueAnimator, times(2)).start();
-        verify(mMockAms, times(2)).notifyMagnificationChanged(eq(displayId),
+        verify(mRequestObserver, times(2)).onFullScreenMagnificationChanged(eq(displayId),
                 eq(INITIAL_MAGNIFICATION_REGION), mConfigCaptor.capture());
         assertConfigEquals(newConfig, mConfigCaptor.getValue());
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 2060223..0fed89b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -151,8 +151,6 @@
                 mock(FullScreenMagnificationController.ControllerContext.class);
         final WindowManagerInternal mockWindowManager = mock(WindowManagerInternal.class);
         when(mockController.getContext()).thenReturn(mContext);
-        when(mockController.getAms()).thenReturn(mMockAccessibilityManagerService);
-        when(mMockAccessibilityManagerService.getTraceManager()).thenReturn(mMockTraceManager);
         when(mockController.getTraceManager()).thenReturn(mMockTraceManager);
         when(mockController.getWindowManager()).thenReturn(mockWindowManager);
         when(mockController.getHandler()).thenReturn(new Handler(mContext.getMainLooper()));
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
index 3fcce92..ec59090 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
@@ -145,9 +145,10 @@
                         mCallbackDelegate, mTraceManager, mScaleProvider));
         mMockConnection = new MockWindowMagnificationConnection(true);
         mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
-        new FullScreenMagnificationControllerStubber(mScreenMagnificationController);
         mMagnificationController = new MagnificationController(mService, globalLock, mContext,
                 mScreenMagnificationController, mWindowMagnificationManager, mScaleProvider);
+        new FullScreenMagnificationControllerStubber(mScreenMagnificationController,
+                mMagnificationController);
 
         mMagnificationController.setMagnificationCapabilities(
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL);
@@ -451,6 +452,29 @@
     }
 
     @Test
+    public void onFullScreenMagnificationChanged_fullScreenEnabled_notifyMagnificationChanged()
+            throws RemoteException {
+        setMagnificationEnabled(MODE_FULLSCREEN);
+
+        final MagnificationConfig config = obtainMagnificationConfig(MODE_FULLSCREEN);
+        mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY,
+                config.getScale(), config.getCenterX(), config.getCenterY(),
+                true, TEST_SERVICE_ID);
+
+        // The first time is triggered when setting magnification enabled. And the second time is
+        // triggered when calling setScaleAndCenter.
+        final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass(
+                MagnificationConfig.class);
+        verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY),
+                eq(FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION),
+                configCaptor.capture());
+        final MagnificationConfig actualConfig = configCaptor.getValue();
+        assertEquals(config.getCenterX(), actualConfig.getCenterX(), 0);
+        assertEquals(config.getCenterY(), actualConfig.getCenterY(), 0);
+        assertEquals(config.getScale(), actualConfig.getScale(), 0);
+    }
+
+    @Test
     public void onAccessibilityActionPerformed_magnifierEnabled_showMagnificationButton()
             throws RemoteException {
         setMagnificationEnabled(MODE_WINDOW);
@@ -679,7 +703,7 @@
             throws RemoteException {
         setMagnificationEnabled(MODE_FULLSCREEN);
         mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY,
-                /* scale= */1, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y,
+                /* scale= */ 1, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y,
                 true, TEST_SERVICE_ID);
 
         mMagnificationController.onFullScreenMagnificationActivationState(TEST_DISPLAY, false);
@@ -884,6 +908,8 @@
     private static class FullScreenMagnificationControllerStubber {
         private static final Region MAGNIFICATION_REGION = new Region(0, 0, 500, 600);
         private final FullScreenMagnificationController mScreenMagnificationController;
+        private final FullScreenMagnificationController.MagnificationInfoChangedCallback
+                mMagnificationChangedCallback;
         private boolean mIsMagnifying = false;
         private float mScale = 1.0f;
         private float mCenterX = MAGNIFICATION_REGION.getBounds().exactCenterX();
@@ -891,8 +917,10 @@
         private int mServiceId = -1;
 
         FullScreenMagnificationControllerStubber(
-                FullScreenMagnificationController screenMagnificationController) {
+                FullScreenMagnificationController screenMagnificationController,
+                FullScreenMagnificationController.MagnificationInfoChangedCallback callback) {
             mScreenMagnificationController = screenMagnificationController;
+            mMagnificationChangedCallback = callback;
             stubMethods();
         }
 
@@ -930,6 +958,14 @@
                 } else {
                     reset();
                 }
+
+
+                final MagnificationConfig config = new MagnificationConfig.Builder().setMode(
+                        MODE_FULLSCREEN).setScale(mScale).setCenterX(mCenterX).setCenterY(
+                        mCenterY).build();
+                mMagnificationChangedCallback.onFullScreenMagnificationChanged(TEST_DISPLAY,
+                        FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION,
+                        config);
                 return true;
             };
             doAnswer(setScaleAndCenterStubAnswer).when(
diff --git a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
new file mode 100644
index 0000000..b154d6f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.transport;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.app.backup.BackupTransport;
+import android.app.backup.RestoreDescription;
+import android.app.backup.RestoreSet;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.backup.IBackupTransport;
+import com.android.internal.backup.ITransportStatusCallback;
+import com.android.internal.infra.AndroidFuture;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.concurrent.CancellationException;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class BackupTransportClientTest {
+
+    private static class TestFuturesFakeTransportBinder extends FakeTransportBinderBase {
+        public final Object mLock = new Object();
+
+        public String mNameCompletedImmediately;
+
+        @GuardedBy("mLock")
+        public AndroidFuture<String> mNameCompletedInFuture;
+
+        @Override public void name(AndroidFuture<String> name) throws RemoteException {
+            name.complete(mNameCompletedImmediately);
+        }
+        @Override public void transportDirName(AndroidFuture<String> name) throws RemoteException {
+            synchronized (mLock) {
+                mNameCompletedInFuture = name;
+                mLock.notifyAll();
+            }
+        }
+    }
+
+    @Test
+    public void testName_completesImmediately_returnsName() throws Exception {
+        TestFuturesFakeTransportBinder binder = new TestFuturesFakeTransportBinder();
+        binder.mNameCompletedImmediately = "fake name";
+
+        BackupTransportClient client = new BackupTransportClient(binder);
+        String name = client.name();
+
+        assertThat(name).isEqualTo("fake name");
+    }
+
+    @Test
+    public void testTransportDirName_completesLater_returnsName() throws Exception {
+        TestFuturesFakeTransportBinder binder = new TestFuturesFakeTransportBinder();
+        BackupTransportClient client = new BackupTransportClient(binder);
+
+        Thread thread = new Thread(() -> {
+            try {
+                String name = client.transportDirName();
+                assertThat(name).isEqualTo("fake name");
+            } catch (Exception ex) {
+                fail("unexpected Exception: " + ex.getClass().getCanonicalName());
+            }
+        });
+
+        thread.start();
+
+        synchronized (binder.mLock) {
+            while (binder.mNameCompletedInFuture == null) {
+                binder.mLock.wait();
+            }
+            assertThat(binder.mNameCompletedInFuture.complete("fake name")).isTrue();
+        }
+
+        thread.join();
+    }
+
+    @Test
+    public void testTransportDirName_canceledBeforeCompletion_throwsException() throws Exception {
+        TestFuturesFakeTransportBinder binder = new TestFuturesFakeTransportBinder();
+        BackupTransportClient client = new BackupTransportClient(binder);
+
+        Thread thread = new Thread(() -> {
+            try {
+                /*String name =*/ client.transportDirName();
+                fail("transportDirName should be cancelled");
+            } catch (CancellationException ex) {
+                // This is expected.
+            } catch (Exception ex) {
+                fail("unexpected Exception: " + ex.getClass().getCanonicalName());
+            }
+        });
+
+        thread.start();
+
+        synchronized (binder.mLock) {
+            while (binder.mNameCompletedInFuture == null) {
+                binder.mLock.wait();
+            }
+            client.onBecomingUnusable();
+        }
+
+        thread.join();
+    }
+
+    private static class TestCallbacksFakeTransportBinder extends FakeTransportBinderBase {
+        public final Object mLock = new Object();
+
+        public int mStatusCompletedImmediately;
+
+        @GuardedBy("mLock")
+        public ITransportStatusCallback mStatusCompletedInFuture;
+
+        @Override public void initializeDevice(ITransportStatusCallback c) throws RemoteException {
+            c.onOperationCompleteWithStatus(mStatusCompletedImmediately);
+        }
+        @Override public void finishBackup(ITransportStatusCallback c) throws RemoteException {
+            synchronized (mLock) {
+                mStatusCompletedInFuture = c;
+                mLock.notifyAll();
+            }
+        }
+    }
+
+    @Test
+    public void testInitializeDevice_completesImmediately_returnsStatus() throws Exception {
+        TestCallbacksFakeTransportBinder binder = new TestCallbacksFakeTransportBinder();
+        binder.mStatusCompletedImmediately = 123;
+
+        BackupTransportClient client = new BackupTransportClient(binder);
+        int status = client.initializeDevice();
+
+        assertThat(status).isEqualTo(123);
+    }
+
+
+    @Test
+    public void testFinishBackup_completesLater_returnsStatus() throws Exception {
+        TestCallbacksFakeTransportBinder binder = new TestCallbacksFakeTransportBinder();
+        BackupTransportClient client = new BackupTransportClient(binder);
+
+        Thread thread = new Thread(() -> {
+            try {
+                int status = client.finishBackup();
+                assertThat(status).isEqualTo(456);
+            } catch (Exception ex) {
+                fail("unexpected Exception: " + ex.getClass().getCanonicalName());
+            }
+        });
+
+        thread.start();
+
+        synchronized (binder.mLock) {
+            while (binder.mStatusCompletedInFuture == null) {
+                binder.mLock.wait();
+            }
+            binder.mStatusCompletedInFuture.onOperationCompleteWithStatus(456);
+        }
+
+        thread.join();
+    }
+
+    @Test
+    public void testFinishBackup_canceledBeforeCompletion_throwsException() throws Exception {
+        TestCallbacksFakeTransportBinder binder = new TestCallbacksFakeTransportBinder();
+        BackupTransportClient client = new BackupTransportClient(binder);
+
+        Thread thread = new Thread(() -> {
+            try {
+                int status = client.finishBackup();
+                assertThat(status).isEqualTo(BackupTransport.TRANSPORT_ERROR);
+            } catch (Exception ex) {
+                fail("unexpected Exception: " + ex.getClass().getCanonicalName());
+            }
+        });
+
+        thread.start();
+
+        synchronized (binder.mLock) {
+            while (binder.mStatusCompletedInFuture == null) {
+                binder.mLock.wait();
+            }
+            client.onBecomingUnusable();
+        }
+
+        thread.join();
+    }
+
+    // Convenience layer so we only need to fake specific methods useful for each test case.
+    private static class FakeTransportBinderBase implements IBackupTransport {
+        @Override public void name(AndroidFuture<String> f) throws RemoteException {}
+        @Override public void transportDirName(AndroidFuture<String> f) throws RemoteException {}
+        @Override public void configurationIntent(AndroidFuture<Intent> f) throws RemoteException {}
+        @Override public void currentDestinationString(AndroidFuture<String> f)
+                throws RemoteException {}
+        @Override public void dataManagementIntent(AndroidFuture<Intent> f)
+                throws RemoteException {}
+        @Override public void dataManagementIntentLabel(AndroidFuture<CharSequence> f)
+                throws RemoteException {}
+        @Override public void initializeDevice(ITransportStatusCallback c) throws RemoteException {}
+        @Override public void clearBackupData(PackageInfo i, ITransportStatusCallback c)
+                throws RemoteException {}
+        @Override public void finishBackup(ITransportStatusCallback c) throws RemoteException {}
+        @Override public void requestBackupTime(AndroidFuture<Long> f) throws RemoteException {}
+        @Override public void performBackup(PackageInfo i, ParcelFileDescriptor fd, int f,
+            ITransportStatusCallback c) throws RemoteException {}
+        @Override public void getAvailableRestoreSets(AndroidFuture<List<RestoreSet>> f)
+                throws RemoteException {}
+        @Override public void getCurrentRestoreSet(AndroidFuture<Long> f) throws RemoteException {}
+        @Override public void startRestore(long t, PackageInfo[] p, ITransportStatusCallback c)
+                throws RemoteException {}
+        @Override public void nextRestorePackage(AndroidFuture<RestoreDescription> f)
+                throws RemoteException {}
+        @Override public void getRestoreData(ParcelFileDescriptor fd, ITransportStatusCallback c)
+                throws RemoteException {}
+        @Override public void finishRestore(ITransportStatusCallback c) throws RemoteException {}
+        @Override public void requestFullBackupTime(AndroidFuture<Long> f) throws RemoteException {}
+        @Override public void performFullBackup(PackageInfo i, ParcelFileDescriptor fd, int f,
+            ITransportStatusCallback c) throws RemoteException {}
+        @Override public void checkFullBackupSize(long s, ITransportStatusCallback c)
+                throws RemoteException {}
+        @Override public void sendBackupData(int n, ITransportStatusCallback c)
+                throws RemoteException {}
+        @Override public void cancelFullBackup(ITransportStatusCallback c) throws RemoteException {}
+        @Override public void isAppEligibleForBackup(PackageInfo p, boolean b,
+            AndroidFuture<Boolean> f) throws RemoteException {}
+        @Override public void getBackupQuota(String s, boolean b, AndroidFuture<Long> f)
+                throws RemoteException {}
+        @Override public void getNextFullRestoreDataChunk(ParcelFileDescriptor fd,
+            ITransportStatusCallback c) throws RemoteException {}
+        @Override public void abortFullRestore(ITransportStatusCallback c) throws RemoteException {}
+        @Override public void getTransportFlags(AndroidFuture<Integer> f) throws RemoteException {}
+        @Override public IBinder asBinder() {
+            return null;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index fe110e5..2398e36 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -3575,11 +3575,11 @@
         setup_DeviceAdminFeatureOff();
         mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE,
-                DevicePolicyManager.CODE_DEVICE_ADMIN_NOT_SUPPORTED);
+                DevicePolicyManager.STATUS_DEVICE_ADMIN_NOT_SUPPORTED);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_FINANCED_DEVICE,
-                DevicePolicyManager.CODE_DEVICE_ADMIN_NOT_SUPPORTED);
+                DevicePolicyManager.STATUS_DEVICE_ADMIN_NOT_SUPPORTED);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_DEVICE_ADMIN_NOT_SUPPORTED);
+                DevicePolicyManager.STATUS_DEVICE_ADMIN_NOT_SUPPORTED);
     }
 
     private void setup_ManagedProfileFeatureOff() throws Exception {
@@ -3611,11 +3611,11 @@
         setup_ManagedProfileFeatureOff();
         mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE,
-                DevicePolicyManager.CODE_OK);
+                DevicePolicyManager.STATUS_OK);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_FINANCED_DEVICE,
-                DevicePolicyManager.CODE_OK);
+                DevicePolicyManager.STATUS_OK);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_MANAGED_USERS_NOT_SUPPORTED);
+                DevicePolicyManager.STATUS_MANAGED_USERS_NOT_SUPPORTED);
     }
 
     private void setup_firstBoot_systemUser() throws Exception {
@@ -3653,15 +3653,15 @@
 
         when(getServices().userManagerForMock.isHeadlessSystemUserMode()).thenReturn(false);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE,
-                DevicePolicyManager.CODE_OK);
+                DevicePolicyManager.STATUS_OK);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_FINANCED_DEVICE,
-                DevicePolicyManager.CODE_OK);
+                DevicePolicyManager.STATUS_OK);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_OK);
+                DevicePolicyManager.STATUS_OK);
 
         when(getServices().userManagerForMock.isHeadlessSystemUserMode()).thenReturn(true);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE,
-                DevicePolicyManager.CODE_OK);
+                DevicePolicyManager.STATUS_OK);
     }
 
     private void setup_systemUserSetupComplete_systemUser() throws Exception {
@@ -3708,11 +3708,11 @@
         setup_systemUserSetupComplete_systemUser();
         mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE,
-                DevicePolicyManager.CODE_USER_SETUP_COMPLETED);
+                DevicePolicyManager.STATUS_USER_SETUP_COMPLETED);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_FINANCED_DEVICE,
-                DevicePolicyManager.CODE_USER_SETUP_COMPLETED);
+                DevicePolicyManager.STATUS_USER_SETUP_COMPLETED);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_OK);
+                DevicePolicyManager.STATUS_OK);
     }
 
     @Test
@@ -3722,22 +3722,22 @@
         mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS);
 
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE,
-                DevicePolicyManager.CODE_HAS_DEVICE_OWNER);
+                DevicePolicyManager.STATUS_HAS_DEVICE_OWNER);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_FINANCED_DEVICE,
-                DevicePolicyManager.CODE_HAS_DEVICE_OWNER);
+                DevicePolicyManager.STATUS_HAS_DEVICE_OWNER);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE, false);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_FINANCED_DEVICE, false);
 
         // COMP mode NOT is allowed.
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, false);
 
         // And other DPCs can NOT provision a managed profile.
         assertCheckProvisioningPreCondition(
                 DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
                 DpmMockContext.ANOTHER_PACKAGE_NAME,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, false,
                 DpmMockContext.ANOTHER_PACKAGE_NAME, DpmMockContext.ANOTHER_UID);
     }
@@ -3759,13 +3759,13 @@
                 eq(UserHandle.getUserHandleForUid(mContext.binder.callingUid))))
                 .thenReturn(UserManager.RESTRICTION_SOURCE_SYSTEM);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, false);
 
         assertCheckProvisioningPreCondition(
                 DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
                 DpmMockContext.ANOTHER_PACKAGE_NAME,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, false,
                 DpmMockContext.ANOTHER_PACKAGE_NAME, DpmMockContext.ANOTHER_UID);
     }
@@ -3794,12 +3794,12 @@
 
         // We can delete the managed profile to create a new one, so provisioning is allowed.
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, false);
         assertCheckProvisioningPreCondition(
                 DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
                 DpmMockContext.ANOTHER_PACKAGE_NAME,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, false,
                 DpmMockContext.ANOTHER_PACKAGE_NAME, DpmMockContext.ANOTHER_UID);
     }
@@ -3823,13 +3823,13 @@
         assertCheckProvisioningPreCondition(
                 DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
                 DpmMockContext.ANOTHER_PACKAGE_NAME,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, false,
                 DpmMockContext.ANOTHER_PACKAGE_NAME, DpmMockContext.ANOTHER_UID);
 
         // But the device owner can still do it because it has set the restriction itself.
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
         assertProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, false);
     }
 
@@ -3901,7 +3901,7 @@
 
         // COMP mode is NOT allowed.
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
     }
 
     private void setup_provisionManagedProfileOneAlreadyExist_primaryUser() throws Exception {
@@ -3935,14 +3935,14 @@
         setup_provisionManagedProfileOneAlreadyExist_primaryUser();
         mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS);
         assertCheckProvisioningPreCondition(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE,
-                DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE);
+                DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE);
     }
 
     @Test
     public void testCheckProvisioningPreCondition_permission() {
         // GIVEN the permission MANAGE_PROFILE_AND_DEVICE_OWNERS is not granted
         assertExpectException(SecurityException.class, /* messageRegex =*/ null,
-                () -> dpm.checkProvisioningPreCondition(
+                () -> dpm.checkProvisioningPrecondition(
                         DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE, "some.package"));
     }
 
@@ -8696,7 +8696,7 @@
     private void assertCheckProvisioningPreCondition(
             String action, String packageName, int provisioningCondition) {
         assertWithMessage("checkProvisioningPreCondition(%s, %s) returning unexpected result",
-                action, packageName).that(dpm.checkProvisioningPreCondition(action, packageName))
+                action, packageName).that(dpm.checkProvisioningPrecondition(action, packageName))
                         .isEqualTo(provisioningCondition);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java b/services/tests/servicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java
index b1b53fc..9e986be 100644
--- a/services/tests/servicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java
@@ -65,7 +65,12 @@
         }
 
         @Override
-        long getMaxSatiatedCirculation() {
+        long getInitialSatiatedConsumptionLimit() {
+            return 0;
+        }
+
+        @Override
+        long getHardSatiatedConsumptionLimit() {
             return 0;
         }
 
@@ -102,7 +107,7 @@
         TrendCalculator trendCalculator = new TrendCalculator();
         mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT, 20);
 
-        trendCalculator.reset(0, null);
+        trendCalculator.reset(0, 0, null);
         assertEquals("Expected not to cross lower threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
                 trendCalculator.getTimeToCrossLowerThresholdMs());
@@ -115,10 +120,10 @@
                 new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT, 1, 0))),
                 mock(AffordabilityChangeListener.class), mEconomicPolicy));
         for (ActionAffordabilityNote note : affordabilityNotes) {
-            note.recalculateModifiedPrice(mEconomicPolicy, 0, "com.test.app");
+            note.recalculateCosts(mEconomicPolicy, 0, "com.test.app");
         }
 
-        trendCalculator.reset(1234, affordabilityNotes);
+        trendCalculator.reset(1234, 1234, affordabilityNotes);
         assertEquals("Expected not to cross lower threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
                 trendCalculator.getTimeToCrossLowerThresholdMs());
@@ -133,32 +138,28 @@
 
         OngoingEvent[] events = new OngoingEvent[]{
                 new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
-                        null, 1, 1),
+                        1, new EconomicPolicy.Cost(1, 4)),
                 new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, "2",
-                        null, 2, 3),
-                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "3",
-                        null, 3, -3),
+                        2, new EconomicPolicy.Cost(3, 6)),
+                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "3", 3,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_TOP_ACTIVITY, 0, 3, 3)),
         };
 
-        trendCalculator.reset(0, null);
+        trendCalculator.reset(0, 100, null);
         for (OngoingEvent event : events) {
             trendCalculator.accept(event);
         }
-        assertEquals("Expected not to cross lower threshold",
-                TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
-                trendCalculator.getTimeToCrossLowerThresholdMs());
+        assertEquals(25_000, trendCalculator.getTimeToCrossLowerThresholdMs());
         assertEquals("Expected not to cross upper threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
                 trendCalculator.getTimeToCrossUpperThresholdMs());
 
         ArraySet<ActionAffordabilityNote> affordabilityNotes = new ArraySet<>();
-        trendCalculator.reset(1234, affordabilityNotes);
+        trendCalculator.reset(1234, 1234, affordabilityNotes);
         for (OngoingEvent event : events) {
             trendCalculator.accept(event);
         }
-        assertEquals("Expected not to cross lower threshold",
-                TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
-                trendCalculator.getTimeToCrossLowerThresholdMs());
+        assertEquals(308_000, trendCalculator.getTimeToCrossLowerThresholdMs());
         assertEquals("Expected not to cross upper threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
                 trendCalculator.getTimeToCrossUpperThresholdMs());
@@ -174,18 +175,19 @@
                 new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 0, 1000))),
                 mock(AffordabilityChangeListener.class), mEconomicPolicy));
         for (ActionAffordabilityNote note : affordabilityNotes) {
-            note.recalculateModifiedPrice(mEconomicPolicy, 0, "com.test.app");
+            note.recalculateCosts(mEconomicPolicy, 0, "com.test.app");
         }
 
         // Balance is already above threshold and events are all positive delta.
         // There should be no time to report.
-        trendCalculator.reset(1234, affordabilityNotes);
+        trendCalculator.reset(1234, 1234, affordabilityNotes);
         trendCalculator.accept(
-                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
-                        null, 1, 1));
+                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "1", 1,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_TOP_ACTIVITY, 1, 1, 1)));
         trendCalculator.accept(
-                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, "2",
-                        null, 2, 3));
+                new OngoingEvent(EconomicPolicy.REWARD_OTHER_USER_INTERACTION, "2", 2,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_OTHER_USER_INTERACTION,
+                                3, 3, 3)));
 
         assertEquals("Expected not to cross lower threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
@@ -196,16 +198,16 @@
 
         // Balance is already below threshold and events are all negative delta.
         // There should be no time to report.
-        trendCalculator.reset(1, affordabilityNotes);
+        trendCalculator.reset(1, 0, affordabilityNotes);
         trendCalculator.accept(
                 new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
-                        null, 1, -1));
+                        1, new EconomicPolicy.Cost(1, 1)));
         trendCalculator.accept(
                 new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, "2",
-                        null, 2, -3));
+                        2, new EconomicPolicy.Cost(3, 3)));
 
         assertEquals("Expected not to cross lower threshold",
-                TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
+                0,
                 trendCalculator.getTimeToCrossLowerThresholdMs());
         assertEquals("Expected not to cross upper threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
@@ -213,7 +215,7 @@
     }
 
     @Test
-    public void testSimpleTrendToThreshold() {
+    public void testSimpleTrendToThreshold_Balance() {
         TrendCalculator trendCalculator = new TrendCalculator();
         mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 20);
 
@@ -222,18 +224,19 @@
                 new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0))),
                 mock(AffordabilityChangeListener.class), mEconomicPolicy));
         for (ActionAffordabilityNote note : affordabilityNotes) {
-            note.recalculateModifiedPrice(mEconomicPolicy, 0, "com.test.app");
+            note.recalculateCosts(mEconomicPolicy, 0, "com.test.app");
         }
 
         // Balance is below threshold and events are all positive delta.
         // Should report the correct time to the upper threshold.
-        trendCalculator.reset(0, affordabilityNotes);
+        trendCalculator.reset(0, 1000, affordabilityNotes);
         trendCalculator.accept(
-                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "1",
-                        null, 1, 1));
+                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "1", 1,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_TOP_ACTIVITY, 1, 1, 1)));
         trendCalculator.accept(
-                new OngoingEvent(EconomicPolicy.REWARD_OTHER_USER_INTERACTION, "2",
-                        null, 2, 3));
+                new OngoingEvent(EconomicPolicy.REWARD_OTHER_USER_INTERACTION, "2", 2,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_OTHER_USER_INTERACTION,
+                                3, 3, 3)));
 
         assertEquals("Expected not to cross lower threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
@@ -242,13 +245,13 @@
 
         // Balance is above the threshold and events are all negative delta.
         // Should report the correct time to the lower threshold.
-        trendCalculator.reset(40, affordabilityNotes);
+        trendCalculator.reset(40, 100, affordabilityNotes);
         trendCalculator.accept(
                 new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
-                        null, 1, -1));
+                        1, new EconomicPolicy.Cost(1, 1)));
         trendCalculator.accept(
                 new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, "2",
-                        null, 2, -3));
+                        2, new EconomicPolicy.Cost(3, 3)));
 
         assertEquals(5_000, trendCalculator.getTimeToCrossLowerThresholdMs());
         assertEquals("Expected not to cross upper threshold",
@@ -257,6 +260,123 @@
     }
 
     @Test
+    public void testSelectCorrectThreshold_Balance() {
+        TrendCalculator trendCalculator = new TrendCalculator();
+        mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 20);
+        mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START, 15);
+        mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_LOW_START, 10);
+        mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_MIN_START, 5);
+
+        ArraySet<ActionAffordabilityNote> affordabilityNotes = new ArraySet<>();
+        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
+                new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0))),
+                mock(AffordabilityChangeListener.class), mEconomicPolicy));
+        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
+                new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START, 1, 0))),
+                mock(AffordabilityChangeListener.class), mEconomicPolicy));
+        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
+                new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_LOW_START, 1, 0))),
+                mock(AffordabilityChangeListener.class), mEconomicPolicy));
+        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
+                new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_MIN_START, 1, 0))),
+                mock(AffordabilityChangeListener.class), mEconomicPolicy));
+        for (ActionAffordabilityNote note : affordabilityNotes) {
+            note.recalculateCosts(mEconomicPolicy, 0, "com.test.app");
+        }
+
+        // Balance is below threshold and events are all positive delta.
+        // Should report the correct time to the correct upper threshold.
+        trendCalculator.reset(0, 10_000, affordabilityNotes);
+        trendCalculator.accept(
+                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "1", 1,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_TOP_ACTIVITY, 1, 1, 1)));
+
+        assertEquals("Expected not to cross lower threshold",
+                TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
+                trendCalculator.getTimeToCrossLowerThresholdMs());
+        assertEquals(5_000, trendCalculator.getTimeToCrossUpperThresholdMs());
+
+        // Balance is above the threshold and events are all negative delta.
+        // Should report the correct time to the correct lower threshold.
+        trendCalculator.reset(30, 500, affordabilityNotes);
+        trendCalculator.accept(
+                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
+                        1, new EconomicPolicy.Cost(1, 1)));
+
+        assertEquals(10_000, trendCalculator.getTimeToCrossLowerThresholdMs());
+        assertEquals("Expected not to cross upper threshold",
+                TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
+                trendCalculator.getTimeToCrossUpperThresholdMs());
+    }
+
+    @Test
+    public void testTrendsToBothThresholds_Balance() {
+        TrendCalculator trendCalculator = new TrendCalculator();
+        mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 20);
+        mEconomicPolicy.mEventCosts.put(AlarmManagerEconomicPolicy.ACTION_ALARM_CLOCK, 50);
+
+        ArraySet<ActionAffordabilityNote> affordabilityNotes = new ArraySet<>();
+        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
+                new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0))),
+                mock(AffordabilityChangeListener.class), mEconomicPolicy));
+        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
+                new AnticipatedAction(AlarmManagerEconomicPolicy.ACTION_ALARM_CLOCK, 1, 0))),
+                mock(AffordabilityChangeListener.class), mEconomicPolicy));
+        for (ActionAffordabilityNote note : affordabilityNotes) {
+            note.recalculateCosts(mEconomicPolicy, 0, "com.test.app");
+        }
+
+        // Balance is between both thresholds and events are mixed positive/negative delta.
+        // Should report the correct time to each threshold.
+        trendCalculator.reset(35, 10_000, affordabilityNotes);
+        trendCalculator.accept(
+                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "1", 1,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_TOP_ACTIVITY, 3, 3, 3)));
+        trendCalculator.accept(
+                new OngoingEvent(EconomicPolicy.REWARD_OTHER_USER_INTERACTION, "2", 2,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_OTHER_USER_INTERACTION, 2,
+                                2, 2)));
+        trendCalculator.accept(
+                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING, "3",
+                        3, new EconomicPolicy.Cost(2, 2)));
+        trendCalculator.accept(
+                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "4",
+                        4, new EconomicPolicy.Cost(3, 3)));
+
+        assertEquals(3_000, trendCalculator.getTimeToCrossLowerThresholdMs());
+        assertEquals(3_000, trendCalculator.getTimeToCrossUpperThresholdMs());
+    }
+
+    @Test
+    public void testSimpleTrendToThreshold_ConsumptionLimit() {
+        TrendCalculator trendCalculator = new TrendCalculator();
+        mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 20);
+
+        ArraySet<ActionAffordabilityNote> affordabilityNotes = new ArraySet<>();
+        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
+                new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0))),
+                mock(AffordabilityChangeListener.class), mEconomicPolicy));
+        for (ActionAffordabilityNote note : affordabilityNotes) {
+            note.recalculateCosts(mEconomicPolicy, 0, "com.test.app");
+        }
+
+        // Events are all negative delta. Consumable credits will run out before app's balance.
+        // Should report the correct time to the lower threshold.
+        trendCalculator.reset(10000, 40, affordabilityNotes);
+        trendCalculator.accept(
+                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
+                        1, new EconomicPolicy.Cost(1, 10)));
+        trendCalculator.accept(
+                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, "2",
+                        2, new EconomicPolicy.Cost(3, 40)));
+
+        assertEquals(10_000, trendCalculator.getTimeToCrossLowerThresholdMs());
+        assertEquals("Expected not to cross upper threshold",
+                TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
+                trendCalculator.getTimeToCrossUpperThresholdMs());
+    }
+
+    @Test
     public void testSelectCorrectThreshold() {
         TrendCalculator trendCalculator = new TrendCalculator();
         mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 20);
@@ -278,68 +398,45 @@
                 new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_MIN_START, 1, 0))),
                 mock(AffordabilityChangeListener.class), mEconomicPolicy));
         for (ActionAffordabilityNote note : affordabilityNotes) {
-            note.recalculateModifiedPrice(mEconomicPolicy, 0, "com.test.app");
+            note.recalculateCosts(mEconomicPolicy, 0, "com.test.app");
         }
 
-        // Balance is below threshold and events are all positive delta.
-        // Should report the correct time to the correct upper threshold.
-        trendCalculator.reset(0, affordabilityNotes);
+        // Balance is above threshold, consumable credits is 0, and events are all positive delta.
+        // There should be no time to the upper threshold since consumable credits is the limiting
+        // factor.
+        trendCalculator.reset(10_000, 0, affordabilityNotes);
         trendCalculator.accept(
-                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "1",
-                        null, 1, 1));
+                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "1", 1,
+                        new EconomicPolicy.Reward(EconomicPolicy.REWARD_TOP_ACTIVITY, 1, 1, 1)));
 
         assertEquals("Expected not to cross lower threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
                 trendCalculator.getTimeToCrossLowerThresholdMs());
-        assertEquals(5_000, trendCalculator.getTimeToCrossUpperThresholdMs());
-
-        // Balance is above the threshold and events are all negative delta.
-        // Should report the correct time to the correct lower threshold.
-        trendCalculator.reset(30, affordabilityNotes);
-        trendCalculator.accept(
-                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
-                        null, 1, -1));
-
-        assertEquals(10_000, trendCalculator.getTimeToCrossLowerThresholdMs());
         assertEquals("Expected not to cross upper threshold",
                 TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
                 trendCalculator.getTimeToCrossUpperThresholdMs());
-    }
 
-    @Test
-    public void testTrendsToBothThresholds() {
-        TrendCalculator trendCalculator = new TrendCalculator();
-        mEconomicPolicy.mEventCosts.put(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 20);
-        mEconomicPolicy.mEventCosts.put(AlarmManagerEconomicPolicy.ACTION_ALARM_CLOCK, 50);
+        // Balance is above threshold, consumable credits is low, and events are all negative delta.
+        trendCalculator.reset(10_000, 4, affordabilityNotes);
+        trendCalculator.accept(
+                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
+                        1, new EconomicPolicy.Cost(1, 10)));
 
-        ArraySet<ActionAffordabilityNote> affordabilityNotes = new ArraySet<>();
-        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
-                new AnticipatedAction(JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 1, 0))),
-                mock(AffordabilityChangeListener.class), mEconomicPolicy));
-        affordabilityNotes.add(new ActionAffordabilityNote(new ActionBill(List.of(
-                new AnticipatedAction(AlarmManagerEconomicPolicy.ACTION_ALARM_CLOCK, 1, 0))),
-                mock(AffordabilityChangeListener.class), mEconomicPolicy));
-        for (ActionAffordabilityNote note : affordabilityNotes) {
-            note.recalculateModifiedPrice(mEconomicPolicy, 0, "com.test.app");
-        }
+        assertEquals(4000, trendCalculator.getTimeToCrossLowerThresholdMs());
+        assertEquals("Expected not to cross upper threshold",
+                TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
+                trendCalculator.getTimeToCrossUpperThresholdMs());
 
-        // Balance is between both thresholds and events are mixed positive/negative delta.
-        // Should report the correct time to each threshold.
-        trendCalculator.reset(35, affordabilityNotes);
+        // Balance is above threshold, consumable credits is 0, and events are all negative delta.
+        // Time to the lower threshold should be 0 since consumable credits is already 0.
+        trendCalculator.reset(10_000, 0, affordabilityNotes);
         trendCalculator.accept(
-                new OngoingEvent(EconomicPolicy.REWARD_TOP_ACTIVITY, "1",
-                        null, 1, 3));
-        trendCalculator.accept(
-                new OngoingEvent(EconomicPolicy.REWARD_OTHER_USER_INTERACTION, "2",
-                        null, 2, 2));
-        trendCalculator.accept(
-                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING, "3",
-                        null, 3, -2));
-        trendCalculator.accept(
-                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "4",
-                        null, 4, -3));
+                new OngoingEvent(JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, "1",
+                        1, new EconomicPolicy.Cost(1, 10)));
 
-        assertEquals(3_000, trendCalculator.getTimeToCrossLowerThresholdMs());
-        assertEquals(3_000, trendCalculator.getTimeToCrossUpperThresholdMs());
+        assertEquals(0, trendCalculator.getTimeToCrossLowerThresholdMs());
+        assertEquals("Expected not to cross upper threshold",
+                TrendCalculator.WILL_NOT_CROSS_THRESHOLD,
+                trendCalculator.getTimeToCrossUpperThresholdMs());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java b/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java
index 4a25323..22dcf84 100644
--- a/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java
+++ b/services/tests/servicestests/src/com/android/server/tare/LedgerTest.java
@@ -54,13 +54,13 @@
     @Test
     public void testMultipleTransactions() {
         final Ledger ledger = new Ledger();
-        ledger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 5));
+        ledger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 5, 0));
         assertEquals(5, ledger.getCurrentBalance());
         assertEquals(5, ledger.get24HourSum(1, 60_000));
-        ledger.recordTransaction(new Ledger.Transaction(2000, 2000, 1, null, 25));
+        ledger.recordTransaction(new Ledger.Transaction(2000, 2000, 1, null, 25, 0));
         assertEquals(30, ledger.getCurrentBalance());
         assertEquals(30, ledger.get24HourSum(1, 60_000));
-        ledger.recordTransaction(new Ledger.Transaction(5000, 5500, 1, null, -10));
+        ledger.recordTransaction(new Ledger.Transaction(5000, 5500, 1, null, -10, 5));
         assertEquals(20, ledger.getCurrentBalance());
         assertEquals(20, ledger.get24HourSum(1, 60_000));
     }
@@ -68,13 +68,13 @@
     @Test
     public void test24HourSum() {
         final Ledger ledger = new Ledger();
-        ledger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 500));
+        ledger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 500, 0));
         assertEquals(500, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
         ledger.recordTransaction(
-                new Ledger.Transaction(2 * HOUR_IN_MILLIS, 3 * HOUR_IN_MILLIS, 1, null, 2500));
+                new Ledger.Transaction(2 * HOUR_IN_MILLIS, 3 * HOUR_IN_MILLIS, 1, null, 2500, 0));
         assertEquals(3000, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
         ledger.recordTransaction(
-                new Ledger.Transaction(4 * HOUR_IN_MILLIS, 4 * HOUR_IN_MILLIS, 1, null, 1));
+                new Ledger.Transaction(4 * HOUR_IN_MILLIS, 4 * HOUR_IN_MILLIS, 1, null, 1, 0));
         assertEquals(3001, ledger.get24HourSum(1, 24 * HOUR_IN_MILLIS));
         assertEquals(2501, ledger.get24HourSum(1, 25 * HOUR_IN_MILLIS));
         assertEquals(2501, ledger.get24HourSum(1, 26 * HOUR_IN_MILLIS));
@@ -93,17 +93,17 @@
 
         final long now = getCurrentTimeMillis();
         Ledger.Transaction transaction1 = new Ledger.Transaction(
-                now - 48 * HOUR_IN_MILLIS, now - 40 * HOUR_IN_MILLIS, 1, null, 4800);
+                now - 48 * HOUR_IN_MILLIS, now - 40 * HOUR_IN_MILLIS, 1, null, 4800, 0);
         Ledger.Transaction transaction2 = new Ledger.Transaction(
-                now - 24 * HOUR_IN_MILLIS, now - 23 * HOUR_IN_MILLIS, 1, null, 600);
+                now - 24 * HOUR_IN_MILLIS, now - 23 * HOUR_IN_MILLIS, 1, null, 600, 0);
         Ledger.Transaction transaction3 = new Ledger.Transaction(
-                now - 22 * HOUR_IN_MILLIS, now - 21 * HOUR_IN_MILLIS, 1, null, 600);
+                now - 22 * HOUR_IN_MILLIS, now - 21 * HOUR_IN_MILLIS, 1, null, 600, 0);
         // Instant event
         Ledger.Transaction transaction4 = new Ledger.Transaction(
-                now - 20 * HOUR_IN_MILLIS, now - 20 * HOUR_IN_MILLIS, 1, null, 500);
+                now - 20 * HOUR_IN_MILLIS, now - 20 * HOUR_IN_MILLIS, 1, null, 500, 0);
         // Recent event
         Ledger.Transaction transaction5 = new Ledger.Transaction(
-                now - 5 * MINUTE_IN_MILLIS, now - MINUTE_IN_MILLIS, 1, null, 400);
+                now - 5 * MINUTE_IN_MILLIS, now - MINUTE_IN_MILLIS, 1, null, 400, 0);
         ledger.recordTransaction(transaction1);
         ledger.recordTransaction(transaction2);
         ledger.recordTransaction(transaction3);
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
index e1a4989..01e306e 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -61,8 +61,6 @@
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.internal.app.IBatteryStats;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -106,8 +104,6 @@
     @Mock
     private IBinder mVibrationToken;
     @Mock
-    private IBatteryStats mIBatteryStatsMock;
-    @Mock
     private VibrationConfig mVibrationConfigMock;
 
     private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
@@ -178,8 +174,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -197,8 +193,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -219,8 +215,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(15L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(15L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -252,8 +248,8 @@
         thread.cancel();
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), anyLong());
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
 
@@ -404,8 +400,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -427,8 +423,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibration);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -446,8 +442,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
-        verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
+        verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong());
+        verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.IGNORED_UNSUPPORTED);
         assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments().isEmpty());
@@ -466,8 +462,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(40L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(40L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -486,8 +482,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock, never()).noteVibratorOn(eq(UID), anyLong());
-        verify(mIBatteryStatsMock, never()).noteVibratorOff(eq(UID));
+        verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong());
+        verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.IGNORED_UNSUPPORTED);
         assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments().isEmpty());
@@ -536,8 +532,8 @@
         waitForCompletion(thread);
 
         // Use first duration the vibrator is turned on since we cannot estimate the clicks.
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -573,8 +569,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(100L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
@@ -666,8 +662,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
         verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId));
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
@@ -690,8 +686,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
         verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
         verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
@@ -728,8 +724,8 @@
         VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
         verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
         verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
@@ -777,13 +773,13 @@
         controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
         controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
 
-        InOrder batterVerifier = inOrder(mIBatteryStatsMock);
-        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
-        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
-        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(10L));
-        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
-        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(20L));
-        batterVerifier.verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        InOrder batteryVerifier = inOrder(mManagerHooks);
+        batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
+        batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
+        batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
+        batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
+        batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
+        batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
 
         verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
         assertFalse(thread.getVibrators().get(1).isVibrating());
@@ -952,8 +948,8 @@
 
         waitForCompletion(thread);
 
-        verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(80L));
-        verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(80L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
         verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
         verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
         verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
@@ -1300,7 +1296,7 @@
 
     private VibrationThread startThreadAndDispatcher(Vibration vib) {
         VibrationThread thread = new VibrationThread(vib, mVibrationSettings, mEffectAdapter,
-                createVibratorControllers(), mWakeLock, mIBatteryStatsMock, mManagerHooks);
+                createVibratorControllers(), mWakeLock, mManagerHooks);
         doAnswer(answer -> {
             thread.vibratorComplete(answer.getArgument(0));
             return null;
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 52975ef..19111e5 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -71,6 +71,7 @@
 import android.os.test.TestLooper;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
@@ -78,6 +79,7 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.internal.app.IBatteryStats;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.internal.util.test.FakeSettingsProviderRule;
 import com.android.server.LocalServices;
@@ -144,6 +146,8 @@
     private AppOpsManager mAppOpsManagerMock;
     @Mock
     private IInputManager mIInputManagerMock;
+    @Mock
+    private IBatteryStats mBatteryStatsMock;
 
     private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
 
@@ -152,12 +156,14 @@
     private FakeVibrator mVibrator;
     private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
     private VibratorManagerService.ExternalVibratorService mExternalVibratorService;
+    private VibrationConfig mVibrationConfig;
 
     @Before
     public void setUp() throws Exception {
         mTestLooper = new TestLooper();
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
         InputManager inputManager = InputManager.resetInstance(mIInputManagerMock);
+        mVibrationConfig = new VibrationConfig(mContextSpy.getResources());
 
         ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
         when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
@@ -222,6 +228,11 @@
                     }
 
                     @Override
+                    IBatteryStats getBatteryStatsService() {
+                        return mBatteryStatsMock;
+                    }
+
+                    @Override
                     VibratorController createVibratorController(int vibratorId,
                             VibratorController.OnVibrationCompleteListener listener) {
                         return mVibratorProviders.get(vibratorId)
@@ -382,6 +393,11 @@
         inOrderVerifier.verify(listenerMock).onVibrating(eq(true));
         inOrderVerifier.verify(listenerMock).onVibrating(eq(false));
         inOrderVerifier.verifyNoMoreInteractions();
+
+        InOrder batteryVerifier = inOrder(mBatteryStatsMock);
+        batteryVerifier.verify(mBatteryStatsMock)
+                .noteVibratorOn(UID, 40 + mVibrationConfig.getRampDownDurationMs());
+        batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID);
     }
 
     @Test
@@ -731,6 +747,12 @@
         // Wait before checking it never played a second effect.
         assertFalse(waitUntil(s -> mVibratorProviders.get(1).getEffectSegments().size() > 1,
                 service, /* timeout= */ 50));
+
+        // The time estimate is recorded when the vibration starts, repeating vibrations
+        // are capped at BATTERY_STATS_REPEATING_VIBRATION_DURATION (=5000).
+        verify(mBatteryStatsMock).noteVibratorOn(UID, 5000);
+        // The second vibration shouldn't have recorded that the vibrators were turned on.
+        verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
     }
 
     @Test
@@ -765,6 +787,9 @@
         when(mIInputManagerMock.getVibratorIds(eq(1))).thenReturn(new int[]{1});
         when(mIInputManagerMock.getInputDevice(eq(1))).thenReturn(createInputDeviceWithVibrator(1));
         setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
+        // Mock alarm intensity equals to default value to avoid scaling in this test.
+        setUserSetting(Settings.System.ALARM_VIBRATION_INTENSITY,
+                mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_ALARM));
         VibratorManagerService service = createSystemReadyService();
 
         CombinedVibration effect = CombinedVibration.createParallel(
@@ -804,6 +829,9 @@
         mockVibrators(1, 2);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
         mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        // Mock alarm intensity equals to default value to avoid scaling in this test.
+        setUserSetting(Settings.System.ALARM_VIBRATION_INTENSITY,
+                mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_ALARM));
         VibratorManagerService service = createSystemReadyService();
         // The native callback will be dispatched manually in this test.
         mTestLooper.stopAutoDispatchAndIgnoreExceptions();
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index f57c32c..0394a54 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -4189,7 +4189,8 @@
      * {@link UiccSlotMapping} which consist of both physical slot index and port index.
      * Logical slot is the slot that is seen by modem. Physical slot is the actual physical slot.
      * Port index is the index (enumerated value) for the associated port available on the SIM.
-     * Each physical slot can have multiple ports if multi-enabled profile(MEP) is supported.
+     * Each physical slot can have multiple ports if
+     * {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP} is supported.
      *
      * Example: no. of logical slots 1 and physical slots 2 do not support MEP, each physical slot
      * has one port:
@@ -4285,11 +4286,11 @@
     /**
      * Get the mapping from logical slots to physical sim slots and port indexes. Initially the
      * logical slot index was mapped to physical slot index, but with support for multi-enabled
-     * profile(MEP) logical slot is now mapped to port index.
+     * profile(MEP){@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP},logical slot is now mapped to
+     * port index.
      *
      * @return a collection of {@link UiccSlotMapping} which indicates the mapping from logical
      *         slots to ports and physical slots.
-     *
      * @hide
      */
     @SystemApi
diff --git a/telephony/java/android/telephony/UiccCardInfo.java b/telephony/java/android/telephony/UiccCardInfo.java
index 30ca162..3843a62 100644
--- a/telephony/java/android/telephony/UiccCardInfo.java
+++ b/telephony/java/android/telephony/UiccCardInfo.java
@@ -17,6 +17,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.pm.PackageManager;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -147,9 +148,10 @@
      * Note that this field may be omitted if the caller does not have the correct permissions
      * (see {@link TelephonyManager#getUiccCardsInfo()}).
      *
-     * @deprecated with support for MEP(multiple enabled profile), a SIM card can have more than one
-     * ICCID active at the same time.Instead use {@link UiccPortInfo#getIccId()} to retrieve ICCID.
-     * To find {@link UiccPortInfo} use {@link UiccCardInfo#getPorts()}
+     * @deprecated with support for MEP(multiple enabled profile)
+     * {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, a SIM card can have more than one
+     * ICCID active at the same time. Instead use {@link UiccPortInfo#getIccId()} to retrieve ICCID.
+     * To find {@link UiccPortInfo} use {@link UiccCardInfo#getPorts()}.
      *
      * @throws UnsupportedOperationException if the calling app's target SDK is T and beyond.
      */
@@ -192,11 +194,11 @@
     }
 
     /*
-     * Whether the UICC card supports multiple enable profile(MEP)
+     * Whether the UICC card supports multiple enabled profile(MEP)
      * UICCs are generally MEP disabled, there can be only one active profile on the physical
      * sim card.
      *
-     * @return {@code true} if the eUICC is supporting multiple enabled profile(MEP).
+     * @return {@code true} if the UICC is supporting multiple enabled profile(MEP).
      */
     public boolean isMultipleEnabledProfilesSupported() {
         return mIsMultipleEnabledProfilesSupported;
@@ -205,6 +207,9 @@
     /**
      * Get information regarding port, ICCID and its active status.
      *
+     * For device which support {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, it should return
+     * more than one {@link UiccPortInfo} object if the card is eUICC.
+     *
      * @return Collection of {@link UiccPortInfo}
      */
     public @NonNull Collection<UiccPortInfo> getPorts() {
diff --git a/telephony/java/android/telephony/UiccPortInfo.java b/telephony/java/android/telephony/UiccPortInfo.java
index d1838c0..6fb0470 100644
--- a/telephony/java/android/telephony/UiccPortInfo.java
+++ b/telephony/java/android/telephony/UiccPortInfo.java
@@ -29,7 +29,9 @@
  * Per GSMA SGP.22 V3.0, a port is a logical entity to which an active UICC profile can be bound on
  * a UICC card. If UICC supports 2 ports, then the port index is numbered 0,1.
  * Each port index is unique within an UICC, but not necessarily unique across UICC’s.
- * For UICC's does not support MEP(Multi-enabled profile), just return the default port index 0.
+ * For UICC's does not support MEP(Multi-enabled profile)
+ * {@link android.content.pm.PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, just return the default
+ * port index 0.
  */
 public final class UiccPortInfo implements Parcelable{
     private final String mIccId;
diff --git a/telephony/java/android/telephony/UiccSlotInfo.java b/telephony/java/android/telephony/UiccSlotInfo.java
index 17f34db..17ce450 100644
--- a/telephony/java/android/telephony/UiccSlotInfo.java
+++ b/telephony/java/android/telephony/UiccSlotInfo.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.content.pm.PackageManager;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -225,6 +226,9 @@
     /**
      * Get Information regarding port, iccid and its active status.
      *
+     * For device which support {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP}, it should return
+     * more than one {@link UiccPortInfo} object if the card is eUICC.
+     *
      * @return Collection of {@link UiccPortInfo}
      */
     public @NonNull Collection<UiccPortInfo> getPorts() {
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index b6ae530..4820d33 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -37,6 +37,7 @@
 import android.telephony.SubscriptionInfo;
 import android.telephony.TelephonyFrameworkInitializer;
 import android.telephony.TelephonyManager;
+import android.telephony.UiccCardInfo;
 import android.telephony.euicc.EuiccCardManager.ResetOption;
 import android.util.Log;
 
@@ -931,6 +932,21 @@
      * intent to prompt the user to accept the download. The caller should also be authorized to
      * manage the subscription to be downloaded.
      *
+     * <p>If device support {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP} and
+     * switchAfterDownload is {@code true}, the subscription will be enabled on an esim port based
+     * on the following selection rules:
+     * <ul>
+     *    <li>In SS(Single SIM) mode, if the embedded slot already has an active port, then download
+     *    and enable the subscription on this port.
+     *    <li>In SS mode, if the embedded slot is not active, then try to download and enable the
+     *    subscription on the default port 0 of eUICC.
+     *    <li>In DSDS mode, find first available port to download and enable the subscription.
+     *    (see {@link #isSimPortAvailable(int)})
+     *</ul>
+     * If there is no available port, an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR}
+     * will be returned in the callback intent to prompt the user to disable an already-active
+     * subscription.
+     *
      * @param subscription the subscription to download.
      * @param switchAfterDownload if true, the profile will be activated upon successful download.
      * @param callbackIntent a PendingIntent to launch when the operation completes.
@@ -1141,14 +1157,25 @@
      * intent to prompt the user to accept the download. The caller should also be authorized to
      * manage the subscription to be enabled.
      *
-     * <p> From Android T, devices might support MEP(Multiple Enabled Profiles), the subscription
-     * can be installed on different port from the eUICC. Calling apps with carrier privilege
-     * (see {@link TelephonyManager#hasCarrierPrivileges}) over the currently active subscriptions
-     * can use {@link #switchToSubscription(int, int, PendingIntent)} to specify which port to
-     * enable the subscription. Otherwise, use this API to enable the subscription on the eUICC
-     * and the platform will internally resolve a port. If there is no available port,
-     * an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} might be returned in the callback
-     * intent to prompt the user to disable an already-active subscription.
+     * <p> From Android T, devices might support {@link PackageManager#FEATURE_TELEPHONY_EUICC_MEP},
+     * the subscription can be installed on different port from the eUICC. Calling apps with
+     * carrier privilege (see {@link TelephonyManager#hasCarrierPrivileges}) over the currently
+     * active subscriptions can use {@link #switchToSubscription(int, int, PendingIntent)} to
+     * specify which port to enable the subscription. Otherwise, use this API to enable the
+     * subscription on the eUICC and the platform will internally resolve a port based on following
+     * rules:
+     * <ul>
+     *    <li>always use the default port 0 is eUICC does not support MEP.
+     *    <li>In SS(Single SIM) mode, if the embedded slot already has an active port, then enable
+     *    the subscription on this port.
+     *    <li>In SS mode, if the embedded slot is not active, then try to enable the subscription on
+     *    the default port 0 of eUICC.
+     *    <li>In DSDS mode, find first available port to enable the subscription.
+     *    (see {@link #isSimPortAvailable(int)})
+     *</ul>
+     * If there is no available port, an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR}
+     * will be returned in the callback intent to prompt the user to disable an already-active
+     * subscription.
      *
      * @param subscriptionId the ID of the subscription to enable. May be
      *     {@link android.telephony.SubscriptionManager#INVALID_SUBSCRIPTION_ID} to deactivate the
@@ -1197,7 +1224,15 @@
      *
      * <p> If the caller is passing invalid port index,
      * an {@link #EMBEDDED_SUBSCRIPTION_RESULT_ERROR} with detailed error code
-     * {@link #ERROR_INVALID_PORT} will be returned.
+     * {@link #ERROR_INVALID_PORT} will be returned. The port index is invalid if one of the
+     * following requirements is met:
+     * <ul>
+     *     <li>index is beyond the range of {@link UiccCardInfo#getPorts()}.
+     *     <li>In SS(Single SIM) mode, the embedded slot already has an active port with different
+     *     port index.
+     *     <li>In DSDS mode, if the psim slot is active and the embedded slot already has an active
+     *     empty port with different port index.
+     * </ul>
      *
      * <p> Depending on the target port and permission check,
      * an {@link #EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR} might be returned to the callback
@@ -1522,8 +1557,8 @@
 
     /**
      * Returns whether the passing portIndex is available.
-     * A port is available if it has no profiles enabled on it or calling app has carrier privilege
-     * over the profile installed on the selected port.
+     * A port is available if it is active without enabled profile on it or
+     * calling app has carrier privilege over the profile installed on the selected port.
      * Always returns false if the cardId is a physical card.
      *
      * @param portIndex is an enumeration of the ports available on the UICC.